From bd088731b08ceb7ca90543e18b19cbf70a21c6c8 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Thu, 12 Aug 2021 16:45:27 +0000 Subject: [PATCH 001/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 2eae223f7f..71da3eb0af 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=2.4.0-M2 +version=2.4.0-SNAPSHOT org.gradlee.caching=true org.gradle.daemon=true org.gradle.parallel=true From cfe842fa63982bd766189e9786440b0a58e0d04d Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 7 Sep 2021 15:46:48 -0400 Subject: [PATCH 002/737] GH-1352: Add RabbitStreamTemplate See https://github.com/spring-projects/spring-amqp/issues/1352 Initial commit of Stream Producer support. * Resolve PR Comments. * Make TestContainers Optional * Add test for null-returning MPP - send skipped. * Fix latch for additional messages sent. * Remove queues after test. --- .../stream/listener/ConsumerCustomizer.java | 4 +- .../stream/producer/ProducerCustomizer.java | 34 +++ .../producer/RabbitStreamOperations.java | 74 +++++++ .../stream/producer/RabbitStreamTemplate.java | 206 ++++++++++++++++++ .../stream/producer/StreamSendException.java | 43 ++++ .../rabbit/stream/producer/package-info.java | 5 + .../support/StreamMessageProperties.java | 6 + .../DefaultStreamMessageConverter.java | 12 +- .../rabbit/stream/support/package-info.java | 5 + .../listener/AbstractIntegrationTests.java | 33 ++- .../stream/listener/RabbitListenerTests.java | 52 ++++- 11 files changed, 453 insertions(+), 21 deletions(-) create mode 100644 spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/ProducerCustomizer.java create mode 100644 spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations.java create mode 100644 spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java create mode 100644 spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/StreamSendException.java create mode 100644 spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/package-info.java create mode 100644 spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/package-info.java diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/ConsumerCustomizer.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/ConsumerCustomizer.java index 501cb3ffe5..f1a7f7d9dc 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/ConsumerCustomizer.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/ConsumerCustomizer.java @@ -21,7 +21,9 @@ import com.rabbitmq.stream.ConsumerBuilder; /** - * Customizer for {@link ConsumerBuilder}. + * Customizer for {@link ConsumerBuilder}. The first parameter should be the bean name (or + * listener id) of the component that calls this customizer. Refer to the RabbitMQ Stream + * Java Client for customization options. * * @author Gary Russell * @since 2.4 diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/ProducerCustomizer.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/ProducerCustomizer.java new file mode 100644 index 0000000000..e615d85d1c --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/ProducerCustomizer.java @@ -0,0 +1,34 @@ +/* + * Copyright 2021 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.rabbit.stream.producer; + +import java.util.function.BiConsumer; + +import com.rabbitmq.stream.ProducerBuilder; + +/** + * Called to enable customization of the {@link ProducerBuilder} when a new producer is + * created. The first parameter should be the bean name of the component that calls this + * customizer. Refer to the RabbitMQ Stream Java Client for customization options. + * + * @author Gary Russell + * @since 2.4 + * + */ +@FunctionalInterface +public interface ProducerCustomizer extends BiConsumer { +} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations.java new file mode 100644 index 0000000000..3481da72ff --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations.java @@ -0,0 +1,74 @@ +/* + * Copyright 2021 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.rabbit.stream.producer; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessagePostProcessor; +import org.springframework.lang.Nullable; +import org.springframework.util.concurrent.ListenableFuture; + +import com.rabbitmq.stream.MessageBuilder; + +/** + * Provides methods for sending messages using a RabbitMQ Stream producer. + * + * @author Gary Russell + * @since 2.4 + * + */ +public interface RabbitStreamOperations extends AutoCloseable { + + /** + * Send a Spring AMQP message. + * @param message the message. + * @return a future to indicate success/failure. + */ + ListenableFuture send(Message message); + + /** + * Convert to and send a Spring AMQP message. + * @param message the payload. + * @return a future to indicate success/failure. + */ + ListenableFuture convertAndSend(Object message); + + /** + * Convert to and send a Spring AMQP message. If a {@link MessagePostProcessor} is + * provided and returns {@code null}, the message is not sent and the future is + * completed with {@code false}. + * @param message the payload. + * @param mpp a message post processor. + * @return a future to indicate success/failure. + */ + ListenableFuture convertAndSend(Object message, @Nullable MessagePostProcessor mpp); + + /** + * Send a native stream message. + * @param message the message. + * @return a future to indicate success/failure. + * @see #messageBuilder() + */ + ListenableFuture send(com.rabbitmq.stream.Message message); + + /** + * Returns the producer's {@link MessageBuilder} to create native stream messages. + * @return the builder. + * @see #send(com.rabbitmq.stream.Message) + */ + MessageBuilder messageBuilder(); + +} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java new file mode 100644 index 0000000000..88a67a6483 --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java @@ -0,0 +1,206 @@ +/* + * Copyright 2021 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.rabbit.stream.producer; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessagePostProcessor; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.amqp.support.converter.SimpleMessageConverter; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.core.log.LogAccessor; +import org.springframework.lang.Nullable; +import org.springframework.rabbit.stream.support.StreamMessageProperties; +import org.springframework.rabbit.stream.support.converter.DefaultStreamMessageConverter; +import org.springframework.rabbit.stream.support.converter.StreamMessageConverter; +import org.springframework.util.Assert; +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.concurrent.SettableListenableFuture; + +import com.rabbitmq.stream.ConfirmationHandler; +import com.rabbitmq.stream.Constants; +import com.rabbitmq.stream.Environment; +import com.rabbitmq.stream.MessageBuilder; +import com.rabbitmq.stream.Producer; +import com.rabbitmq.stream.ProducerBuilder; + +/** + * Default implementation of {@link RabbitStreamOperations}. + * + * @author Gary Russell + * @since 2.4 + * + */ +public class RabbitStreamTemplate implements RabbitStreamOperations, BeanNameAware { + + protected final LogAccessor logger = new LogAccessor(getClass()); // NOSONAR + + private final Environment environment; + + private final String streamName; + + private MessageConverter messageConverter = new SimpleMessageConverter(); + + private StreamMessageConverter streamConverter = new DefaultStreamMessageConverter(); + + private Producer producer; + + private String beanName; + + private ProducerCustomizer producerCustomizer = (name, builder) -> { }; + + /** + * Construct an instance with the provided {@link Environment}. + * @param environment the environment. + * @param streamName the stream name. + */ + public RabbitStreamTemplate(Environment environment, String streamName) { + Assert.notNull(environment, "'environment' cannot be null"); + Assert.notNull(streamName, "'streamName' cannot be null"); + this.environment = environment; + this.streamName = streamName; + } + + + private synchronized Producer createOrGetProducer() { + if (this.producer == null) { + ProducerBuilder builder = this.environment.producerBuilder(); + builder.stream(this.streamName); + this.producerCustomizer.accept(this.beanName, builder); + this.producer = builder.build(); + } + return this.producer; + } + + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + /** + * Set a converter for {@link #convertAndSend(Object)} operations. + * @param messageConverter the converter. + */ + public void setMessageConverter(MessageConverter messageConverter) { + Assert.notNull(messageConverter, "'messageConverter' cannot be null"); + this.messageConverter = messageConverter; + } + + /** + * Set a converter to convert from {@link Message} to {@link com.rabbitmq.stream.Message} + * for {@link #send(Message)} and {@link #convertAndSend(Object)} methods. + * @param streamConverter the converter. + */ + public void setStreamConverter(StreamMessageConverter streamConverter) { + Assert.notNull(streamConverter, "'streamConverter' cannot be null"); + this.streamConverter = streamConverter; + } + + /** + * Used to customize the {@link ProducerBuilder} before the {@link Producer} is built. + * @param producerCustomizer the customizer; + */ + public void setProducerCustomizer(ProducerCustomizer producerCustomizer) { + Assert.notNull(producerCustomizer, "'producerCustomizer' cannot be null"); + this.producerCustomizer = producerCustomizer; + } + + @Override + public ListenableFuture send(Message message) { + SettableListenableFuture future = new SettableListenableFuture<>(); + createOrGetProducer().send(this.streamConverter.fromMessage(message), handleConfirm(future)); + return future; + } + + @Override + public ListenableFuture convertAndSend(Object message) { + return convertAndSend(message, null); + } + + @Override + public ListenableFuture convertAndSend(Object message, @Nullable MessagePostProcessor mpp) { + Message message2 = this.messageConverter.toMessage(message, new StreamMessageProperties()); + Assert.notNull(message2, "The message converter returned null"); + if (mpp != null) { + message2 = mpp.postProcessMessage(message2); + if (message2 == null) { + this.logger.debug("Message Post Processor returned null, message not sent"); + SettableListenableFuture future = new SettableListenableFuture<>(); + future.set(false); + return future; + } + } + return send(message2); + } + + + @Override + public ListenableFuture send(com.rabbitmq.stream.Message message) { + SettableListenableFuture future = new SettableListenableFuture<>(); + createOrGetProducer().send(message, handleConfirm(future)); + return future; + } + + @Override + public MessageBuilder messageBuilder() { + return createOrGetProducer().messageBuilder(); + } + + private ConfirmationHandler handleConfirm(SettableListenableFuture future) { + return confStatus -> { + if (confStatus.isConfirmed()) { + future.set(true); + } + else { + int code = confStatus.getCode(); + String errorMessage; + switch (code) { + case Constants.CODE_MESSAGE_ENQUEUEING_FAILED: + errorMessage = "Message Enqueueing Failed"; + break; + case Constants.CODE_PRODUCER_CLOSED: + errorMessage = "Producer Closed"; + break; + case Constants.CODE_PRODUCER_NOT_AVAILABLE: + errorMessage = "Producer Not Available"; + break; + case Constants.CODE_PUBLISH_CONFIRM_TIMEOUT: + errorMessage = "Publish Confirm Timeout"; + break; + default: + errorMessage = "Unknown code: " + code; + break; + } + future.setException(new StreamSendException(errorMessage, code)); + } + }; + } + + /** + * {@inheritDoc} + *

+ * Close the underlying producer; a new producer will be created on the next + * operation that requires one. + */ + @Override + public synchronized void close() { + if (this.producer != null) { + this.producer.close(); + this.producer = null; + } + } + +} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/StreamSendException.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/StreamSendException.java new file mode 100644 index 0000000000..cd6e6bc789 --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/StreamSendException.java @@ -0,0 +1,43 @@ +/* + * Copyright 2021 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.rabbit.stream.producer; + +import org.springframework.amqp.AmqpException; + +/** + * Used to complete the future exceptionally when sending fails. + * + * @author Gary Russell + * @since 2.4 + * + */ +public class StreamSendException extends AmqpException { + + private static final long serialVersionUID = 1L; + + private final int confirmationCode; + /** + * Construct an instance with the provided message. + * @param message the message. + * @param code the confirmation code. + */ + public StreamSendException(String message, int code) { + super(message); + this.confirmationCode = code; + } + +} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/package-info.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/package-info.java new file mode 100644 index 0000000000..ebf6d3c825 --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/package-info.java @@ -0,0 +1,5 @@ +/** + * Provides classes for stream producers. + */ +@org.springframework.lang.NonNullApi +package org.springframework.rabbit.stream.producer; diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/StreamMessageProperties.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/StreamMessageProperties.java index a9d6281c72..23e170f781 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/StreamMessageProperties.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/StreamMessageProperties.java @@ -48,6 +48,12 @@ public class StreamMessageProperties extends MessageProperties { private String replyToGroupId; + /** + * Create a new instance. + */ + public StreamMessageProperties() { + } + /** * Create a new instance with the provided context. * @param context the context. diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverter.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverter.java index 9f68c6e238..a49bf67ad4 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverter.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverter.java @@ -46,10 +46,10 @@ */ public class DefaultStreamMessageConverter implements StreamMessageConverter { - private final Supplier builderSupplier; - private final Charset charset = StandardCharsets.UTF_8; + private Supplier builderSupplier; + /** * Construct an instance using a {@link WrapperMessageBuilder}. */ @@ -65,6 +65,14 @@ public DefaultStreamMessageConverter(@Nullable Codec codec) { this.builderSupplier = () -> codec.messageBuilder(); } + /** + * Set a supplier for a message builder. + * @param builderSupplier the supplier. + */ + public void setBuilderSupplier(Supplier builderSupplier) { + this.builderSupplier = builderSupplier; + } + @Override public Message toMessage(Object object, StreamMessageProperties messageProperties) throws MessageConversionException { Assert.isInstanceOf(com.rabbitmq.stream.Message.class, object); diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/package-info.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/package-info.java new file mode 100644 index 0000000000..c0eb78691c --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/package-info.java @@ -0,0 +1,5 @@ +/** + * Provides support classes. + */ +@org.springframework.lang.NonNullApi +package org.springframework.rabbit.stream.support; diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/AbstractIntegrationTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/AbstractIntegrationTests.java index 9aac7226cc..083684c57e 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/AbstractIntegrationTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/AbstractIntegrationTests.java @@ -31,15 +31,32 @@ public abstract class AbstractIntegrationTests { static final GenericContainer RABBITMQ; static { - String image = "pivotalrabbitmq/rabbitmq-stream"; - String cache = System.getenv().get("IMAGE_CACHE"); - if (cache != null) { - image = cache + image; + if (System.getProperty("spring.rabbit.use.local.server") == null) { + String image = "pivotalrabbitmq/rabbitmq-stream"; + String cache = System.getenv().get("IMAGE_CACHE"); + if (cache != null) { + image = cache + image; + } + RABBITMQ = new GenericContainer<>(DockerImageName.parse(image)) + .withExposedPorts(5672, 15672, 5552) + .withStartupTimeout(Duration.ofMinutes(2)); + RABBITMQ.start(); } - RABBITMQ = new GenericContainer<>(DockerImageName.parse(image)) - .withExposedPorts(5672, 15672, 5552) - .withStartupTimeout(Duration.ofMinutes(2)); - RABBITMQ.start(); + else { + RABBITMQ = null; + } + } + + static int amqpPort() { + return RABBITMQ != null ? RABBITMQ.getMappedPort(5672) : 5672; + } + + static int managementPort() { + return RABBITMQ != null ? RABBITMQ.getMappedPort(15672) : 15672; + } + + static int streamPort() { + return RABBITMQ != null ? RABBITMQ.getMappedPort(5552) : 5552; } } diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java index 5ce1602c3f..0d53093cab 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java @@ -18,9 +18,13 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Queue; @@ -36,6 +40,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.rabbit.stream.config.StreamRabbitListenerContainerFactory; +import org.springframework.rabbit.stream.producer.RabbitStreamTemplate; +import org.springframework.rabbit.stream.support.StreamMessageProperties; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; @@ -59,11 +65,30 @@ public class RabbitListenerTests extends AbstractIntegrationTests { @Autowired Config config; + @AfterAll + static void deleteQueues() { + try (Environment environment = Config.environment()) { + environment.deleteStream("test.stream.queue1"); + environment.deleteStream("test.stream.queue2"); + environment.deleteStream("stream.created.over.amqp"); + } + } + @Test - void simple(@Autowired RabbitTemplate template) throws InterruptedException { - template.convertAndSend("test.stream.queue1", "foo"); + void simple(@Autowired RabbitStreamTemplate template) throws Exception { + Future future = template.convertAndSend("foo"); + assertThat(future.get(10, TimeUnit.SECONDS)).isTrue(); + future = template.convertAndSend("bar", msg -> msg); + assertThat(future.get(10, TimeUnit.SECONDS)).isTrue(); + future = template.send(new org.springframework.amqp.core.Message("baz".getBytes(), + new StreamMessageProperties())); + assertThat(future.get(10, TimeUnit.SECONDS)).isTrue(); + future = template.send(template.messageBuilder().addData("qux".getBytes()).build()); + assertThat(future.get(10, TimeUnit.SECONDS)).isTrue(); + future = template.convertAndSend("bar", msg -> null); + assertThat(future.get(10, TimeUnit.SECONDS)).isFalse(); assertThat(this.config.latch1.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(this.config.received).isEqualTo("foo"); + assertThat(this.config.received).containsExactly("foo", "bar", "baz", "qux"); assertThat(this.config.id).isEqualTo("test"); } @@ -77,7 +102,7 @@ void nativeMsg(@Autowired RabbitTemplate template) throws InterruptedException { @Test void queueOverAmqp() throws Exception { - Client client = new Client("http://guest:guest@localhost:" + RABBITMQ.getMappedPort(15672) + "/api"); + Client client = new Client("http://guest:guest@localhost:" + managementPort() + "/api"); QueueInfo queue = client.getQueue("/", "stream.created.over.amqp"); assertThat(queue.getArguments().get("x-queue-type")).isEqualTo("stream"); } @@ -86,11 +111,11 @@ void queueOverAmqp() throws Exception { @EnableRabbit public static class Config { - final CountDownLatch latch1 = new CountDownLatch(1); + final CountDownLatch latch1 = new CountDownLatch(4); final CountDownLatch latch2 = new CountDownLatch(1); - volatile String received; + final List received = new ArrayList<>(); volatile Message receivedNative; @@ -99,9 +124,9 @@ public static class Config { volatile String id; @Bean - Environment environment() { + static Environment environment() { return Environment.builder() - .addressResolver(add -> new Address("localhost", RABBITMQ.getMappedPort(5552))) + .addressResolver(add -> new Address("localhost", streamPort())) .build(); } @@ -133,7 +158,7 @@ RabbitListenerContainerFactory rabbitListenerContainerF @RabbitListener(queues = "test.stream.queue1") void listen(String in) { - this.received = in; + this.received.add(in); this.latch1.countDown(); } @@ -160,7 +185,7 @@ void nativeMsg(Message in, Context context) { @Bean CachingConnectionFactory cf() { - return new CachingConnectionFactory(RABBITMQ.getContainerIpAddress(), RABBITMQ.getFirstMappedPort()); + return new CachingConnectionFactory("localhost", amqpPort()); } @Bean @@ -168,6 +193,13 @@ RabbitTemplate template(CachingConnectionFactory cf) { return new RabbitTemplate(cf); } + @Bean + RabbitStreamTemplate streamTemplate1(Environment env) { + RabbitStreamTemplate template = new RabbitStreamTemplate(env, "test.stream.queue1"); + template.setProducerCustomizer((name, builder) -> builder.name("test")); + return template; + } + @Bean RabbitAdmin admin(CachingConnectionFactory cf) { return new RabbitAdmin(cf); From c9f1b4c81df6acc4ce4dd644f85d327ee52c742b Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 8 Sep 2021 12:39:32 -0400 Subject: [PATCH 003/737] GH-1352: Fix New Sonar Issues --- .../rabbit/stream/producer/RabbitStreamTemplate.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java index 88a67a6483..32079daaa7 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java @@ -86,7 +86,7 @@ private synchronized Producer createOrGetProducer() { } @Override - public void setBeanName(String name) { + public synchronized void setBeanName(String name) { this.beanName = name; } @@ -113,7 +113,7 @@ public void setStreamConverter(StreamMessageConverter streamConverter) { * Used to customize the {@link ProducerBuilder} before the {@link Producer} is built. * @param producerCustomizer the customizer; */ - public void setProducerCustomizer(ProducerCustomizer producerCustomizer) { + public synchronized void setProducerCustomizer(ProducerCustomizer producerCustomizer) { Assert.notNull(producerCustomizer, "'producerCustomizer' cannot be null"); this.producerCustomizer = producerCustomizer; } From c8663b3748521c79f228d0b5d5423f30b5186612 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 9 Sep 2021 12:14:16 -0400 Subject: [PATCH 004/737] GH-1365: Recover Manual Declarations Resolves https://github.com/spring-projects/spring-amqp/issues/1365 * Verify bean definitions are not added to `manualDeclarables`. --- .../amqp/rabbit/core/RabbitAdmin.java | 120 +++++++++++++++++- .../amqp/rabbit/core/RabbitAdminTests.java | 61 +++++++++ src/reference/asciidoc/amqp.adoc | 14 ++ src/reference/asciidoc/whats-new.adoc | 7 +- 4 files changed, 198 insertions(+), 4 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java index fa77077b97..1c0da149cf 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java @@ -19,10 +19,14 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Properties; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; @@ -63,6 +67,7 @@ import org.springframework.util.StringUtils; import com.rabbitmq.client.AMQP.Queue.DeclareOk; +import com.rabbitmq.client.AMQP.Queue.DeleteOk; import com.rabbitmq.client.AMQP.Queue.PurgeOk; import com.rabbitmq.client.Channel; @@ -124,6 +129,8 @@ public class RabbitAdmin implements AmqpAdmin, ApplicationContextAware, Applicat private final ConnectionFactory connectionFactory; + private final Map manualDeclarables = Collections.synchronizedMap(new LinkedHashMap<>()); + private String beanName; private RetryTemplate retryTemplate; @@ -142,6 +149,8 @@ public class RabbitAdmin implements AmqpAdmin, ApplicationContextAware, Applicat private boolean explicitDeclarationsOnly; + private boolean redeclareManualDeclarations; + private volatile boolean running = false; private volatile DeclarationExceptionEvent lastDeclarationExceptionEvent; @@ -220,6 +229,9 @@ public void declareExchange(final Exchange exchange) { try { this.rabbitTemplate.execute(channel -> { declareExchanges(channel, exchange); + if (this.redeclareManualDeclarations) { + this.manualDeclarables.put(exchange.getName(), exchange); + } return null; }); } @@ -238,6 +250,7 @@ public boolean deleteExchange(final String exchangeName) { try { channel.exchangeDelete(exchangeName); + removeExchangeBindings(exchangeName); } catch (@SuppressWarnings(UNUSED) IOException e) { return false; @@ -246,6 +259,24 @@ public boolean deleteExchange(final String exchangeName) { }); } + private void removeExchangeBindings(final String exchangeName) { + this.manualDeclarables.remove(exchangeName); + synchronized (this.manualDeclarables) { + Iterator> iterator = this.manualDeclarables.entrySet().iterator(); + while (iterator.hasNext()) { + Entry next = iterator.next(); + if (next.getValue() instanceof Binding) { + Binding binding = (Binding) next.getValue(); + if ((!binding.isDestinationQueue() && binding.getDestination().equals(exchangeName)) + || binding.getExchange().equals(exchangeName)) { + iterator.remove(); + } + } + } + } + } + + // Queue operations /** @@ -266,7 +297,11 @@ public String declareQueue(final Queue queue) { try { return this.rabbitTemplate.execute(channel -> { DeclareOk[] declared = declareQueues(channel, queue); - return declared.length > 0 ? declared[0].getQueue() : null; + String result = declared.length > 0 ? declared[0].getQueue() : null; + if (this.redeclareManualDeclarations) { + this.manualDeclarables.put(result, queue); + } + return result; }); } catch (AmqpException e) { @@ -303,6 +338,7 @@ public boolean deleteQueue(final String queueName) { return this.rabbitTemplate.execute(channel -> { // NOSONAR never returns null try { channel.queueDelete(queueName); + removeQueueBindings(queueName); } catch (@SuppressWarnings(UNUSED) IOException e) { return false; @@ -316,11 +352,28 @@ public boolean deleteQueue(final String queueName) { "Delete a queue from the broker if unused and empty (when corresponding arguments are true") public void deleteQueue(final String queueName, final boolean unused, final boolean empty) { this.rabbitTemplate.execute(channel -> { - channel.queueDelete(queueName, unused, empty); + DeleteOk queueDelete = channel.queueDelete(queueName, unused, empty); + removeQueueBindings(queueName); return null; }); } + private void removeQueueBindings(final String queueName) { + this.manualDeclarables.remove(queueName); + synchronized (this.manualDeclarables) { + Iterator> iterator = this.manualDeclarables.entrySet().iterator(); + while (iterator.hasNext()) { + Entry next = iterator.next(); + if (next.getValue() instanceof Binding) { + Binding binding = (Binding) next.getValue(); + if (binding.isDestinationQueue() && binding.getDestination().equals(queueName)) { + iterator.remove(); + } + } + } + } + } + @Override @ManagedOperation(description = "Purge a queue and optionally don't wait for the purge to occur") public void purgeQueue(final String queueName, final boolean noWait) { @@ -352,6 +405,9 @@ public void declareBinding(final Binding binding) { try { this.rabbitTemplate.execute(channel -> { declareBindings(channel, binding); + if (this.redeclareManualDeclarations) { + this.manualDeclarables.put(binding.toString(), binding); + } return null; }); } @@ -377,6 +433,7 @@ public void removeBinding(final Binding binding) { channel.exchangeUnbind(binding.getDestination(), binding.getExchange(), binding.getRoutingKey(), binding.getArguments()); } + this.manualDeclarables.remove(binding.toString()); return null; }); } @@ -444,6 +501,37 @@ public void setExplicitDeclarationsOnly(boolean explicitDeclarationsOnly) { this.explicitDeclarationsOnly = explicitDeclarationsOnly; } + /** + * Normally, when a connection is recovered, the admin only recovers auto-delete queues, + * etc, that are declared as beans in the application context. When this is true, it + * will also redeclare any manually declared {@link Declarable}s via admin methods. + * @return true to redeclare. + * @since 2.4 + */ + public boolean isRedeclareManualDeclarations() { + return this.redeclareManualDeclarations; + } + + /** + * Normally, when a connection is recovered, the admin only recovers auto-delete + * queues, etc, that are declared as beans in the application context. When this is + * true, it will also redeclare any manually declared {@link Declarable}s via admin + * methods. When a queue or exhange is deleted, it will not longer be recovered, nor + * will any corresponding bindings. + * @param redeclareManualDeclarations true to redeclare. + * @since 2.4 + * @see #declareQueue(Queue) + * @see #declareExchange(Exchange) + * @see #declareBinding(Binding) + * @see #deleteQueue(String) + * @see #deleteExchange(String) + * @see #removeBinding(Binding) + * @see #resetAllManualDeclarations() + */ + public void setRedeclareManualDeclarations(boolean redeclareManualDeclarations) { + this.redeclareManualDeclarations = redeclareManualDeclarations; + } + /** * Set a retry template for auto declarations. There is a race condition with * auto-delete, exclusive queues in that the queue might still exist for a short time, @@ -597,7 +685,7 @@ public void initialize() { } } - if (exchanges.size() == 0 && queues.size() == 0 && bindings.size() == 0) { + if (exchanges.size() == 0 && queues.size() == 0 && bindings.size() == 0 && this.manualDeclarables.size() == 0) { this.logger.debug("Nothing to declare"); return; } @@ -607,10 +695,36 @@ public void initialize() { declareBindings(channel, bindings.toArray(new Binding[bindings.size()])); return null; }); + if (this.manualDeclarables.size() > 0) { + synchronized (this.manualDeclarables) { + this.logger.debug("Redeclaring manually declared Declarables"); + for (Declarable dec : this.manualDeclarables.values()) { + if (dec instanceof Queue) { + declareQueue((Queue) dec); + } + else if (dec instanceof Exchange) { + declareExchange((Exchange) dec); + } + else { + declareBinding((Binding) dec); + } + } + } + } this.logger.debug("Declarations finished"); } + /** + * Invoke this method to prevent the admin from recovering any declarations made + * by calls to {@code declare*()} methods. + * @since 2.4 + * @see #setRedeclareManualDeclarations(boolean) + */ + public void resetAllManualDeclarations() { + this.manualDeclarables.clear(); + } + private void processDeclarables(Collection contextExchanges, Collection contextQueues, Collection contextBindings) { diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java index d298c3f73c..04b804ef48 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java @@ -387,6 +387,67 @@ public void testLeaderLocator() throws Exception { cf.destroy(); } + @Test + void manualDeclarations() { + CachingConnectionFactory cf = new CachingConnectionFactory( + RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); + RabbitAdmin admin = new RabbitAdmin(cf); + GenericApplicationContext applicationContext = new GenericApplicationContext(); + admin.setApplicationContext(applicationContext); + admin.setRedeclareManualDeclarations(true); + applicationContext.registerBean("admin", RabbitAdmin.class, () -> admin); + applicationContext.registerBean("beanQueue", Queue.class, + () -> new Queue("thisOneShouldntBeInTheManualDecs", false, true, true)); + applicationContext.registerBean("beanEx", DirectExchange.class, + () -> new DirectExchange("thisOneShouldntBeInTheManualDecs", false, true)); + applicationContext.registerBean("beanBinding", Binding.class, + () -> new Binding("thisOneShouldntBeInTheManualDecs", DestinationType.QUEUE, + "thisOneShouldntBeInTheManualDecs", "test", null)); + applicationContext.refresh(); + Map declarables = TestUtils.getPropertyValue(admin, "manualDeclarables", Map.class); + assertThat(declarables).hasSize(0); + // check the auto-configured Declarables + RabbitTemplate template = new RabbitTemplate(cf); + template.convertAndSend("thisOneShouldntBeInTheManualDecs", "test", "foo"); + Object received = template.receiveAndConvert("thisOneShouldntBeInTheManualDecs", 5000); + assertThat(received).isEqualTo("foo"); + // manual declarations + admin.declareQueue(new Queue("test1", false, true, true)); + admin.declareQueue(new Queue("test2", false, true, true)); + admin.declareExchange(new DirectExchange("ex1", false, true)); + admin.declareBinding(new Binding("test1", DestinationType.QUEUE, "ex1", "test", null)); + admin.deleteQueue("test2"); + template.execute(chan -> chan.queueDelete("test1")); + cf.resetConnection(); + admin.initialize(); + assertThat(admin.getQueueProperties("test1")).isNotNull(); + assertThat(admin.getQueueProperties("test2")).isNull(); + assertThat(declarables).hasSize(3); + // verify the exchange and binding were recovered too + template.convertAndSend("ex1", "test", "foo"); + received = template.receiveAndConvert("test1", 5000); + assertThat(received).isEqualTo("foo"); + admin.resetAllManualDeclarations(); + assertThat(declarables).hasSize(0); + cf.resetConnection(); + admin.initialize(); + assertThat(admin.getQueueProperties("test1")).isNull(); + admin.declareQueue(new Queue("test1", false, true, true)); + admin.declareExchange(new DirectExchange("ex1", false, true)); + admin.declareBinding(new Binding("test1", DestinationType.QUEUE, "ex1", "test", null)); + admin.declareExchange(new DirectExchange("ex2", false, true)); + admin.declareBinding(new Binding("test1", DestinationType.QUEUE, "ex2", "test", null)); + admin.declareBinding(new Binding("ex1", DestinationType.EXCHANGE, "ex2", "ex1", null)); + assertThat(declarables).hasSize(6); + admin.deleteExchange("ex2"); + assertThat(declarables).hasSize(3); + admin.deleteQueue("test1"); + assertThat(declarables).hasSize(1); + admin.deleteExchange("ex1"); + assertThat(declarables).hasSize(0); + cf.destroy(); + } + @Configuration public static class Config { diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index 2e127824fb..830f9acb56 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -5371,6 +5371,20 @@ Starting with version 2.1, anonymous queues are declared with argument `Queue.X_ This ensures that the queue is declared on the node to which the application is connected. You can revert to the previous behavior by calling `queue.setLeaderLocator(null)` after constructing the instance. +[[declarable-recovery]] +===== Recovering Auto-Delete Declarations + +Normally, the `RabbitAdmin` (s) only recover queues/exchanges/bindings that are declared as beans in the application context; if any such declarations are auto-delete, they will be removed by the broker if the connection is lost. +When the connection is re-established, the admin will redeclare the entities. +Normally, entities created by calling `admin.declareQueue(...)`, `admin.declareExchange(...)` and `admin.declareBinding(...)` will not be recovered. + +Starting with version 2.4, the admin has a new property `redeclareManualDeclarations`; when true, the admin will recover these entities in addition to the beans in the application context. + +Recovery of individual declarations will not be performed if `deleteQueue(...)`, `deleteExchange(...)` or `removeBinding(...)` is called. +Associated bindings are removed from the recoverable entities when queues and exchanges are deleted. + +Finally, calling `resetAllManualDeclarations()` will prevent the recovery of any previously declared entities. + [[broker-events]] ==== Broker Event Listener diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index f625e8a44e..ba34b2f057 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -6,7 +6,12 @@ This section describes the changes between version 2.4 and version 2.4. See <> for changes in previous versions. -==== @RabbitListener Changes +==== `@RabbitListener` Changes `MessageProperties` is now available for argument matching. See <> for more information. + +==== `RabbitAdmin` Changes + +A new property `recoverManualDeclarations` allows recovery of manually declared queues/exchanges/bindings. +See <> for more information. From 2ecde2a282f567cbecd24131dfe5550cf5419d78 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 9 Sep 2021 12:41:01 -0400 Subject: [PATCH 005/737] GH-1352: Add Getter to StreamSendException.confCode --- .../rabbit/stream/producer/StreamSendException.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/StreamSendException.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/StreamSendException.java index cd6e6bc789..de11d4abf3 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/StreamSendException.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/StreamSendException.java @@ -40,4 +40,12 @@ public StreamSendException(String message, int code) { this.confirmationCode = code; } + /** + * Return the confirmation code, if available. + * @return the code. + */ + public int getConfirmationCode() { + return this.confirmationCode; + } + } From 2a7375ca94fc75345fb74430d79102de8c0feb6f Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Fri, 10 Sep 2021 09:44:32 -0400 Subject: [PATCH 006/737] GH-1365: Remove Unused Variable --- .../java/org/springframework/amqp/rabbit/core/RabbitAdmin.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java index 1c0da149cf..9a2be6b7eb 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java @@ -67,7 +67,6 @@ import org.springframework.util.StringUtils; import com.rabbitmq.client.AMQP.Queue.DeclareOk; -import com.rabbitmq.client.AMQP.Queue.DeleteOk; import com.rabbitmq.client.AMQP.Queue.PurgeOk; import com.rabbitmq.client.Channel; @@ -352,7 +351,7 @@ public boolean deleteQueue(final String queueName) { "Delete a queue from the broker if unused and empty (when corresponding arguments are true") public void deleteQueue(final String queueName, final boolean unused, final boolean empty) { this.rabbitTemplate.execute(channel -> { - DeleteOk queueDelete = channel.queueDelete(queueName, unused, empty); + channel.queueDelete(queueName, unused, empty); removeQueueBindings(queueName); return null; }); From c3d7624796d346e82e12dedb78acbff89283ed26 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 14 Sep 2021 13:38:21 -0400 Subject: [PATCH 007/737] GH-1352: StreamRabbitTemplate Improvements Support for SI `RabbitStreamMessageHandler`, which will live in the SCSt RabbitMQ binder until SI 6.0 due to versioning. - expose converters - use producer message builder if no stream converter provided * Remove streams before tests; AfterAll has a timing problem. --- build.gradle | 4 ++-- .../producer/RabbitStreamOperations.java | 23 ++++++++++++++++++- .../stream/producer/RabbitStreamTemplate.java | 19 +++++++++++++++ .../stream/listener/RabbitListenerTests.java | 13 +++++++++-- 4 files changed, 54 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 5f6f9c7ae0..ae85085e4f 100644 --- a/build.gradle +++ b/build.gradle @@ -58,10 +58,10 @@ ext { micrometerVersion = '1.8.0-M2' mockitoVersion = '3.11.2' protonJVersion = '0.33.8' - rabbitmqStreamVersion = '0.1.0' + rabbitmqStreamVersion = '0.3.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.13.0' rabbitmqHttpClientVersion = '3.11.0' - reactorVersion = '2020.0.10' + reactorVersion = '2020.0.11' snappyVersion = '1.1.8.4' springDataCommonsVersion = '2.6.0-M2' springVersion = project.hasProperty('springVersion') ? project.springVersion : '5.3.9' diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations.java index 3481da72ff..bf3a8ea683 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations.java @@ -16,9 +16,12 @@ package org.springframework.rabbit.stream.producer; +import org.springframework.amqp.AmqpException; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessagePostProcessor; +import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.lang.Nullable; +import org.springframework.rabbit.stream.support.converter.StreamMessageConverter; import org.springframework.util.concurrent.ListenableFuture; import com.rabbitmq.stream.MessageBuilder; @@ -65,10 +68,28 @@ public interface RabbitStreamOperations extends AutoCloseable { ListenableFuture send(com.rabbitmq.stream.Message message); /** - * Returns the producer's {@link MessageBuilder} to create native stream messages. + * Return the producer's {@link MessageBuilder} to create native stream messages. * @return the builder. * @see #send(com.rabbitmq.stream.Message) */ MessageBuilder messageBuilder(); + /** + * Return the message converter. + * @return the converter. + */ + MessageConverter messageConverter(); + + /** + * Return the stream message converter. + * @return the converter; + */ + StreamMessageConverter streamMessageConverter(); + + @Override + default void close() throws AmqpException { + // narrow exception to avoid compiler warning - see + // https://bugs.openjdk.java.net/browse/JDK-8155591 + } + } diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java index 32079daaa7..b651832be5 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java @@ -56,6 +56,8 @@ public class RabbitStreamTemplate implements RabbitStreamOperations, BeanNameAwa private StreamMessageConverter streamConverter = new DefaultStreamMessageConverter(); + private boolean streamConverterSet; + private Producer producer; private String beanName; @@ -81,6 +83,10 @@ private synchronized Producer createOrGetProducer() { builder.stream(this.streamName); this.producerCustomizer.accept(this.beanName, builder); this.producer = builder.build(); + if (!this.streamConverterSet) { + ((DefaultStreamMessageConverter) this.streamConverter).setBuilderSupplier( + () -> this.producer.messageBuilder()); + } } return this.producer; } @@ -107,6 +113,7 @@ public void setMessageConverter(MessageConverter messageConverter) { public void setStreamConverter(StreamMessageConverter streamConverter) { Assert.notNull(streamConverter, "'streamConverter' cannot be null"); this.streamConverter = streamConverter; + this.streamConverterSet = true; } /** @@ -118,6 +125,18 @@ public synchronized void setProducerCustomizer(ProducerCustomizer producerCustom this.producerCustomizer = producerCustomizer; } + @Override + public MessageConverter messageConverter() { + return this.messageConverter; + } + + + @Override + public StreamMessageConverter streamMessageConverter() { + return this.streamConverter; + } + + @Override public ListenableFuture send(Message message) { SettableListenableFuture future = new SettableListenableFuture<>(); diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java index 0d53093cab..2c7352cb89 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java @@ -24,7 +24,6 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Queue; @@ -65,7 +64,7 @@ public class RabbitListenerTests extends AbstractIntegrationTests { @Autowired Config config; - @AfterAll +// @AfterAll - causes test to throw errors - need to investigate static void deleteQueues() { try (Environment environment = Config.environment()) { environment.deleteStream("test.stream.queue1"); @@ -140,6 +139,16 @@ public void stop() { @Override public void start() { + try { + env.deleteStream("test.stream.queue1"); + } + catch (Exception e) { + } + try { + env.deleteStream("test.stream.queue2"); + } + catch (Exception e) { + } env.streamCreator().stream("test.stream.queue1").create(); env.streamCreator().stream("test.stream.queue2").create(); } From 226e6b5c9ff75e71236db1811c13fd393b1ae797 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 15 Sep 2021 09:00:23 -0400 Subject: [PATCH 008/737] GH-1352: Fix New Sonar Issue --- .../rabbit/stream/producer/RabbitStreamTemplate.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java index b651832be5..e8c14bc6c6 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java @@ -110,7 +110,7 @@ public void setMessageConverter(MessageConverter messageConverter) { * for {@link #send(Message)} and {@link #convertAndSend(Object)} methods. * @param streamConverter the converter. */ - public void setStreamConverter(StreamMessageConverter streamConverter) { + public synchronized void setStreamConverter(StreamMessageConverter streamConverter) { Assert.notNull(streamConverter, "'streamConverter' cannot be null"); this.streamConverter = streamConverter; this.streamConverterSet = true; From 9dcc6687ed08ad80055be02f817f47b715c5b127 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 16 Sep 2021 15:54:14 -0400 Subject: [PATCH 009/737] Doc: Add Anchors to Container Properties --- src/reference/asciidoc/amqp.adoc | 113 +++++++++++++++---------------- 1 file changed, 54 insertions(+), 59 deletions(-) diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index 830f9acb56..04c3b29a01 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -5708,7 +5708,7 @@ Some properties are not exposed by the namespace. These are indicated by `N/A` for the attribute. .Configuration options for a message listener container -[cols="6l,16,1,1", options="header"] +[cols="8,16,1,1", options="header"] |=== |Property (Attribute) @@ -5716,7 +5716,7 @@ These are indicated by `N/A` for the attribute. |SMLC |DMLC -|ackTimeout +|[[ackTimeout]]<> + (N/A) |When `messagesPerAck` is set, this timeout is used as an alternative to send an ack. @@ -5728,7 +5728,7 @@ See also `messagesPerAck` and `monitorInterval` in this table. a| a|image::images/tickmark.png[] -|acknowledgeMode +|[[acknowledgeMode]]<> + (acknowledge) a| @@ -5743,7 +5743,7 @@ See also `batchSize`. a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|adviceChain +|[[adviceChain]]<> + (advice-chain) |An array of AOP Advice to apply to the listener execution. @@ -5753,7 +5753,7 @@ Note that simple re-connection after an AMQP error is handled by the `CachingCon a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|afterReceivePostProcessors +|[[afterReceivePostProcessors]]<> + (N/A) |An array of `MessagePostProcessor` instances that are invoked before invoking the listener. @@ -5764,8 +5764,7 @@ If a post processor returns `null`, the message is discarded (and acknowledged, a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|alwaysRequeueWith -TxManagerRollback +|[[alwaysRequeueWithTxManagerRollback]]<> + (N/A) |Set to `true` to always requeue messages on rollback when a transaction manager is configured. @@ -5773,7 +5772,7 @@ TxManagerRollback a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|autoDeclare +|[[autoDeclare]]<> + (auto-declare) a|When set to `true` (default), the container uses a `RabbitAdmin` to redeclare all AMQP objects (queues, exchanges, bindings), if it detects that at least one of its queues is missing during startup, perhaps because it is an `auto-delete` or an expired queue, but the redeclaration proceeds if the queue is missing for any reason. @@ -5788,7 +5787,7 @@ Starting with version 1.6, for `autoDeclare` to work, there must be exactly one a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|autoStartup +|[[autoStartup]]<> + (auto-startup) |Flag to indicate that the container should start when the `ApplicationContext` does (as part of the `SmartLifecycle` callbacks, which happen after all beans are initialized). @@ -5797,7 +5796,7 @@ Defaults to `true`, but you can set it to `false` if your broker might not be av a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|batchSize +|[[batchSize]]<> + (transaction-size) (batch-size) @@ -5808,7 +5807,7 @@ If the `prefetchCount` is less than the `batchSize`, it is increased to match th a|image::images/tickmark.png[] a| -|batchingStrategy +|[[batchingStrategy]]<> + (N/A) |The strategy used when debatchng messages. @@ -5818,7 +5817,7 @@ See <> and <>. a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|channelTransacted +|[[channelTransacted]]<> + (channel-transacted) |Boolean flag to signal that all messages should be acknowledged in a transaction (either manually or automatically). @@ -5826,7 +5825,7 @@ a|image::images/tickmark.png[] a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|concurrency +|[[concurrency]]<> + (N/A) |`m-n` The range of concurrent consumers for each listener (min, max). @@ -5836,7 +5835,7 @@ See <>. a|image::images/tickmark.png[] a| -|concurrentConsumers +|[[concurrentConsumers]]<> + (concurrency) |The number of concurrent consumers to initially start for each listener. @@ -5845,7 +5844,7 @@ See <>. a|image::images/tickmark.png[] a| -|connectionFactory +|[[connectionFactory]]<> + (connection-factory) |A reference to the `ConnectionFactory`. @@ -5854,7 +5853,7 @@ When configuring byusing the XML namespace, the default referenced bean name is a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|consecutiveActiveTrigger +|[[consecutiveActiveTrigger]]<> + (min-consecutive-active) |The minimum number of consecutive messages received by a consumer, without a receive timeout occurring, when considering starting a new consumer. @@ -5865,7 +5864,7 @@ Default: 10. a|image::images/tickmark.png[] a| -|consecutiveIdleTrigger +|[[consecutiveIdleTrigger]]<> + (min-consecutive-idle) |The minimum number of receive timeouts a consumer must experience before considering stopping a consumer. @@ -5876,7 +5875,7 @@ Default: 10. a|image::images/tickmark.png[] a| -|consumerBatchEnabled +|[[consumerBatchEnabled]]<> + (batch-enabled) |If the `MessageListener` supports it, setting this to true enables batching of discrete messages, up to `batchSize`; a partial batch will be delivered if no new messages arrive in `receiveTimeout`. @@ -5885,7 +5884,7 @@ When this is false, batching is only supported for batches created by a producer a|image::images/tickmark.png[] a| -|consumerStartTimeout +|[[consumerStartTimeout]]<> + (N/A) |The time in milliseconds to wait for a consumer thread to start. @@ -5898,7 +5897,7 @@ Default: 60000 (one minute). a|image::images/tickmark.png[] a| -|consumerTagStrategy +|[[consumerTagStrategy]]<> + (consumer-tag-strategy) |Set an implementation of <>, enabling the creation of a (unique) tag for each consumer. @@ -5906,7 +5905,7 @@ a| a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|consumersPerQueue +|[[consumersPerQueue]]<> + (consumers-per-queue) |The number of consumers to create for each configured queue. @@ -5915,7 +5914,7 @@ See <>. a| a|image::images/tickmark.png[] -|consumeDelay +|[[consumeDelay]]<> + (N/A) |When using the https://github.com/rabbitmq/rabbitmq-sharding[RabbitMQ Sharding Plugin] with `concurrentConsumers > 1`, there is a race condition that can prevent even distribution of the consumers across the shards. @@ -5925,7 +5924,7 @@ You should experiment with values to determine the suitable delay for your envir a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|debatchingEnabled +|[[debatchingEnabled]]<> + (N/A) |When true, the listener container will debatch batched messages and invoke the listener with each message from the batch. @@ -5937,7 +5936,7 @@ See <> and <>. a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|declarationRetries +|[[declarationRetries]]<> + (declaration-retries) |The number of retry attempts when passive queue declaration fails. @@ -5948,7 +5947,7 @@ Default: Three retries (for a total of four attempts). a|image::images/tickmark.png[] a| -|defaultRequeueRejected +|[[defaultRequeueRejected]]<> + (requeue-rejected) |Determines whether messages that are rejected because the listener threw an exception should be requeued or not. @@ -5957,7 +5956,7 @@ Default: `true`. a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|errorHandler +|[[errorHandler]]<> + (error-handler) |A reference to an `ErrorHandler` strategy for handling any uncaught exceptions that may occur during the execution of the MessageListener. @@ -5966,7 +5965,7 @@ Default: `ConditionalRejectingErrorHandler` a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|exclusive +|[[exclusive]]<> + (exclusive) |Determines whether the single consumer in this container has exclusive access to the queues. @@ -5979,8 +5978,7 @@ Default: `false`. a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|exclusiveConsumer -ExceptionLogger +|[[exclusiveConsumerExceptionLogger]]<> + (N/A) |An exception logger used when an exclusive consumer cannot gain access to a queue. @@ -5989,8 +5987,7 @@ By default, this is logged at the `WARN` level. a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|failedDeclaration -RetryInterval +|[[failedDeclarationRetryInterval]]<> + (failed-declaration -retry-interval) @@ -6001,7 +5998,7 @@ Default: 5000 (five seconds). a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|forceCloseChannel +|[[forceCloseChannel]]<> + (N/A) |If the consumers do not respond to a shutdown within `shutdownTimeout`, if this is `true`, the channel will be closed, causing any unacked messages to be requeued. @@ -6011,7 +6008,7 @@ You can set it to `false` to revert to the previous behavior. a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|globalQos +|[[globalQos]]<> + (global-qos) |When true, the `prefetchCount` is applied globally to the channel rather than to each consumer on the channel. @@ -6032,7 +6029,7 @@ an aggregate of all containers so designated. a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|idleEventInterval +|[[idleEventInterval]]<> + (idle-event-interval) |See <>. @@ -6040,7 +6037,7 @@ a|image::images/tickmark.png[] a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|javaLangErrorHandler +|[[javaLangErrorHandler]]<> + (N/A) |An `AbstractMessageListenerContainer.JavaLangErrorHandler` implementation that is called when a container thread catches an `Error`. @@ -6049,7 +6046,7 @@ The default implementation calls `System.exit(99)`; to revert to the previous be a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|maxConcurrentConsumers +|[[maxConcurrentConsumers]]<> + (max-concurrency) |The maximum number of concurrent consumers to start, if needed, on demand. @@ -6059,7 +6056,7 @@ See <>. a|image::images/tickmark.png[] a| -|messagesPerAck +|[[messagesPerAck]]<> + (N/A) |The number of messages to receive between acks. @@ -6074,7 +6071,7 @@ See also `ackTimeout` in this table. a| a|image::images/tickmark.png[] -|mismatchedQueuesFatal +|[[mismatchedQueuesFatal]]<> + (mismatched-queues-fatal) a|When the container starts, if this property is `true` (default: `false`), the container checks that all queues declared in the context are compatible with queues already on the broker. @@ -6098,7 +6095,7 @@ Applications using lazy listener beans should check the queue arguments before g a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|missingQueuesFatal +|[[missingQueuesFatal]]<> + (missing-queues-fatal) a|When set to `true` (default), if none of the configured queues are available on the broker, it is considered fatal. @@ -6137,7 +6134,7 @@ Applications using lazy listener beans should check the queue(s) before getting a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|monitorInterval +|[[monitorInterval]]<> + (monitor-interval) |With the DMLC, a task is scheduled to run at this interval to monitor the state of the consumers and recover any that have failed. @@ -6145,7 +6142,7 @@ a|image::images/tickmark.png[] a| a|image::images/tickmark.png[] -|noLocal +|[[noLocal]]<> + (N/A) |Set to `true` to disable delivery from the server to consumers messages published on the same channel's connection. @@ -6153,7 +6150,7 @@ a|image::images/tickmark.png[] a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|phase +|[[phase]]<> + (phase) |When `autoStartup` is `true`, the lifecycle phase within which this container should start and stop. @@ -6163,10 +6160,8 @@ The default is `Integer.MAX_VALUE`, meaning the container starts as late as poss a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|possibleAuthentication -FailureFatal -(possible-authentication- -failure-fatal) +|[[possibleAuthenticationFailureFatal]]<> + +(possible-authentication-failure-fatal) a|When set to `true` (default for SMLC), if a `PossibleAuthenticationFailureException` is thrown during connection, it is considered fatal. This causes the application context to fail to initialize during startup (if the container is configured with auto startup). @@ -6204,7 +6199,7 @@ The default retry properties (3 retries at 5 second intervals) can be overridden a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|prefetchCount +|[[prefetchCount]]<> + (prefetch) a|The number of unacknowledged messages that can be outstanding at each consumer. @@ -6225,7 +6220,7 @@ Also see `globalQos`. a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|rabbitAdmin +|[[rabbitAdmin]]<> + (admin) |When a listener container listens to at least one auto-delete queue and it is found to be missing during startup, the container uses a `RabbitAdmin` to declare the queue and any related bindings and exchanges. @@ -6238,7 +6233,7 @@ Defaults to a `RabbitAdmin` that declares all non-conditional elements. a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|receiveTimeout +|[[receiveTimeout]]<> + (receive-timeout) |The maximum time to wait for each message. @@ -6249,7 +6244,7 @@ When `consumerBatchEnabled` is true, a partial batch will be delivered if this t a|image::images/tickmark.png[] a| -|recoveryBackOff +|[[recoveryBackOff]]<> + (recovery-back-off) |Specifies the `BackOff` for intervals between attempts to start a consumer if it fails to start for non-fatal reasons. @@ -6259,7 +6254,7 @@ Mutually exclusive with `recoveryInterval`. a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|recoveryInterval +|[[recoveryInterval]]<> + (recovery-interval) |Determines the time in milliseconds between attempts to start a consumer if it fails to start for non-fatal reasons. @@ -6269,7 +6264,7 @@ Mutually exclusive with `recoveryBackOff`. a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|retryDeclarationInterval +|[[retryDeclarationInterval]]<> + (missing-queue- retry-interval) @@ -6283,7 +6278,7 @@ Default: 60000 (one minute). a|image::images/tickmark.png[] a| -|shutdownTimeout +|[[shutdownTimeout]]<> + (N/A) |When a container shuts down (for example, @@ -6293,7 +6288,7 @@ Defaults to five seconds. a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|startConsumerMinInterval +|[[startConsumerMinInterval]]<> + (min-start-interval) |The time in milliseconds that must elapse before each new consumer is started on demand. @@ -6303,7 +6298,7 @@ Default: 10000 (10 seconds). a|image::images/tickmark.png[] a| -|statefulRetryFatal +|[[statefulRetryFatal]]<> + WithNullMessageId (N/A) @@ -6314,7 +6309,7 @@ Set this to `false` to discard (or route to a dead-letter queue) such messages. a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|stopConsumerMinInterval +|[[stopConsumerMinInterval]]<> + (min-stop-interval) |The time in milliseconds that must elapse before a consumer is stopped since the last consumer was stopped when an idle consumer is detected. @@ -6324,7 +6319,7 @@ Default: 60000 (one minute). a|image::images/tickmark.png[] a| -|taskExecutor +|[[taskExecutor]]<> + (task-executor) |A reference to a Spring `TaskExecutor` (or standard JDK 1.5+ `Executor`) for executing listener invokers. @@ -6333,7 +6328,7 @@ Default is a `SimpleAsyncTaskExecutor`, using internally managed threads. a|image::images/tickmark.png[] a|image::images/tickmark.png[] -|taskScheduler +|[[taskScheduler]]<> + (task-scheduler) |With the DMLC, the scheduler used to run the monitor task at the 'monitorInterval'. @@ -6341,7 +6336,7 @@ a|image::images/tickmark.png[] a| a|image::images/tickmark.png[] -|transactionManager +|[[transactionManager]]<> + (transaction-manager) |External transaction manager for the operation of the listener. From 7e3f7bfc56694f050e8bac0880e83c59592c89f9 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 16 Sep 2021 16:48:51 -0400 Subject: [PATCH 010/737] GH-1352: Docs for RabbitMQ Stream Plugin Support Resolves https://github.com/spring-projects/spring-amqp/issues/1352 --- .../listener/StreamListenerContainer.java | 14 +- src/reference/asciidoc/amqp.adoc | 77 +++++++++- src/reference/asciidoc/index.adoc | 2 + src/reference/asciidoc/stream.adoc | 138 ++++++++++++++++++ 4 files changed, 221 insertions(+), 10 deletions(-) create mode 100644 src/reference/asciidoc/stream.adoc diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java index 4063b935fb..4640defed7 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java @@ -48,7 +48,7 @@ public class StreamListenerContainer implements MessageListenerContainer, BeanNa private final ConsumerBuilder builder; - private StreamMessageConverter messageConverter; + private StreamMessageConverter streamConverter; private ConsumerCustomizer consumerCustomizer = (id, con) -> { }; @@ -78,7 +78,7 @@ public StreamListenerContainer(Environment environment) { public StreamListenerContainer(Environment environment, @Nullable Codec codec) { Assert.notNull(environment, "'environment' cannot be null"); this.builder = environment.consumerBuilder(); - this.messageConverter = new DefaultStreamMessageConverter(codec); + this.streamConverter = new DefaultStreamMessageConverter(codec); } @Override @@ -93,8 +93,8 @@ public void setQueueNames(String... queueNames) { * {@link org.springframework.amqp.core.Message}. * @return the converter. */ - public StreamMessageConverter getMessageConverter() { - return this.messageConverter; + public StreamMessageConverter getStreamConverter() { + return this.streamConverter; } /** @@ -103,9 +103,9 @@ public StreamMessageConverter getMessageConverter() { * {@link org.springframework.amqp.core.Message}. * @param messageConverter the converter. */ - public void setMessageConverter(StreamMessageConverter messageConverter) { + public void setStreamConverter(StreamMessageConverter messageConverter) { Assert.notNull(messageConverter, "'messageConverter' cannot be null"); - this.messageConverter = messageConverter; + this.streamConverter = messageConverter; } /** @@ -189,7 +189,7 @@ public void setupMessageListener(MessageListener messageListener) { ((StreamMessageListener) messageListener).onStreamMessage(message, context); } else { - Message message2 = this.messageConverter.toMessage(message, new StreamMessageProperties(context)); + Message message2 = this.streamConverter.toMessage(message, new StreamMessageProperties(context)); if (messageListener instanceof ChannelAwareMessageListener) { try { ((ChannelAwareMessageListener) messageListener).onMessage(message2, null); diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index 04c3b29a01..4d1fbcbee3 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -5699,7 +5699,7 @@ Enable this feature by calling `ConnectionFactoryUtils.enableAfterCompletionFail ==== Message Listener Container Configuration There are quite a few options for configuring a `SimpleMessageListenerContainer` (SMLC) and a `DirectMessageListenerContainer` (DMLC) related to transactions and quality of service, and some of them interact with each other. -Properties that apply to the SMLC or DMLC are indicated by the check mark in the appropriate column. +Properties that apply to the SMLC, DMLC, or `StreamListenerContainer` (StLC) (see <> are indicated by the check mark in the appropriate column. See <> for information to help you decide which container is appropriate for your application. The following table shows the container property names and their equivalent attribute names (in parentheses) when using the namespace to configure a ``. @@ -5708,13 +5708,14 @@ Some properties are not exposed by the namespace. These are indicated by `N/A` for the attribute. .Configuration options for a message listener container -[cols="8,16,1,1", options="header"] +[cols="8,16,1,1,1", options="header"] |=== |Property (Attribute) |Description |SMLC |DMLC +|StLC |[[ackTimeout]]<> + (N/A) @@ -5727,6 +5728,7 @@ See also `messagesPerAck` and `monitorInterval` in this table. a| a|image::images/tickmark.png[] +a| |[[acknowledgeMode]]<> + (acknowledge) @@ -5742,6 +5744,7 @@ See also `batchSize`. a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[adviceChain]]<> + (advice-chain) @@ -5752,6 +5755,7 @@ Note that simple re-connection after an AMQP error is handled by the `CachingCon a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[afterReceivePostProcessors]]<> + (N/A) @@ -5763,6 +5767,7 @@ If a post processor returns `null`, the message is discarded (and acknowledged, a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[alwaysRequeueWithTxManagerRollback]]<> + (N/A) @@ -5771,6 +5776,7 @@ a|image::images/tickmark.png[] a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[autoDeclare]]<> + (auto-declare) @@ -5786,6 +5792,7 @@ Starting with version 1.6, for `autoDeclare` to work, there must be exactly one a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[autoStartup]]<> + (auto-startup) @@ -5793,6 +5800,7 @@ a|image::images/tickmark.png[] |Flag to indicate that the container should start when the `ApplicationContext` does (as part of the `SmartLifecycle` callbacks, which happen after all beans are initialized). Defaults to `true`, but you can set it to `false` if your broker might not be available on startup and call `start()` later manually when you know the broker is ready. +a|image::images/tickmark.png[] a|image::images/tickmark.png[] a|image::images/tickmark.png[] @@ -5806,6 +5814,7 @@ If the `prefetchCount` is less than the `batchSize`, it is increased to match th a|image::images/tickmark.png[] a| +a| |[[batchingStrategy]]<> + (N/A) @@ -5816,6 +5825,7 @@ See <> and <>. a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[channelTransacted]]<> + (channel-transacted) @@ -5824,6 +5834,7 @@ a|image::images/tickmark.png[] a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[concurrency]]<> + (N/A) @@ -5834,6 +5845,7 @@ See <>. a|image::images/tickmark.png[] a| +a| |[[concurrentConsumers]]<> + (concurrency) @@ -5843,6 +5855,7 @@ See <>. a|image::images/tickmark.png[] a| +a| |[[connectionFactory]]<> + (connection-factory) @@ -5852,6 +5865,7 @@ When configuring byusing the XML namespace, the default referenced bean name is a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[consecutiveActiveTrigger]]<> + (min-consecutive-active) @@ -5863,6 +5877,7 @@ Default: 10. a|image::images/tickmark.png[] a| +a| |[[consecutiveIdleTrigger]]<> + (min-consecutive-idle) @@ -5874,6 +5889,7 @@ Default: 10. a|image::images/tickmark.png[] a| +a| |[[consumerBatchEnabled]]<> + (batch-enabled) @@ -5883,6 +5899,16 @@ When this is false, batching is only supported for batches created by a producer a|image::images/tickmark.png[] a| +a| + +|[[consumerCustomizer]]<> + +(N/A) + +|A `ConsumerCustomizer` bean used to modify stream consumers created by the container. + +a| +a| +a|image::images/tickmark.png[] |[[consumerStartTimeout]]<> + (N/A) @@ -5896,6 +5922,7 @@ Default: 60000 (one minute). a|image::images/tickmark.png[] a| +a| |[[consumerTagStrategy]]<> + (consumer-tag-strategy) @@ -5904,6 +5931,7 @@ a| a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[consumersPerQueue]]<> + (consumers-per-queue) @@ -5913,6 +5941,7 @@ See <>. a| a|image::images/tickmark.png[] +a| |[[consumeDelay]]<> + (N/A) @@ -5923,6 +5952,7 @@ You should experiment with values to determine the suitable delay for your envir a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[debatchingEnabled]]<> + (N/A) @@ -5935,6 +5965,7 @@ See <> and <>. a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[declarationRetries]]<> + (declaration-retries) @@ -5946,6 +5977,7 @@ Default: Three retries (for a total of four attempts). a|image::images/tickmark.png[] a| +a| |[[defaultRequeueRejected]]<> + (requeue-rejected) @@ -5955,6 +5987,7 @@ Default: `true`. a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[errorHandler]]<> + (error-handler) @@ -5964,6 +5997,7 @@ Default: `ConditionalRejectingErrorHandler` a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[exclusive]]<> + (exclusive) @@ -5977,6 +6011,7 @@ Default: `false`. a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[exclusiveConsumerExceptionLogger]]<> + (N/A) @@ -5986,6 +6021,7 @@ By default, this is logged at the `WARN` level. a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[failedDeclarationRetryInterval]]<> + (failed-declaration @@ -5997,6 +6033,7 @@ Default: 5000 (five seconds). a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[forceCloseChannel]]<> + (N/A) @@ -6007,6 +6044,7 @@ You can set it to `false` to revert to the previous behavior. a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[globalQos]]<> + (global-qos) @@ -6016,6 +6054,7 @@ See https://www.rabbitmq.com/amqp-0-9-1-reference.html#basic.qos.global[`basicQo a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |(group) @@ -6028,6 +6067,7 @@ an aggregate of all containers so designated. a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[idleEventInterval]]<> + (idle-event-interval) @@ -6036,6 +6076,7 @@ a|image::images/tickmark.png[] a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[javaLangErrorHandler]]<> + (N/A) @@ -6045,6 +6086,7 @@ The default implementation calls `System.exit(99)`; to revert to the previous be a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[maxConcurrentConsumers]]<> + (max-concurrency) @@ -6055,6 +6097,7 @@ See <>. a|image::images/tickmark.png[] a| +a| |[[messagesPerAck]]<> + (N/A) @@ -6070,6 +6113,7 @@ See also `ackTimeout` in this table. a| a|image::images/tickmark.png[] +a| |[[mismatchedQueuesFatal]]<> + (mismatched-queues-fatal) @@ -6094,6 +6138,7 @@ Applications using lazy listener beans should check the queue arguments before g a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[missingQueuesFatal]]<> + (missing-queues-fatal) @@ -6133,6 +6178,7 @@ Applications using lazy listener beans should check the queue(s) before getting a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[monitorInterval]]<> + (monitor-interval) @@ -6141,6 +6187,7 @@ a|image::images/tickmark.png[] a| a|image::images/tickmark.png[] +a| |[[noLocal]]<> + (N/A) @@ -6149,6 +6196,7 @@ a|image::images/tickmark.png[] a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[phase]]<> + (phase) @@ -6159,6 +6207,7 @@ The default is `Integer.MAX_VALUE`, meaning the container starts as late as poss a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[possibleAuthenticationFailureFatal]]<> + (possible-authentication-failure-fatal) @@ -6198,6 +6247,7 @@ The default retry properties (3 retries at 5 second intervals) can be overridden a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[prefetchCount]]<> + (prefetch) @@ -6219,6 +6269,7 @@ Also see `globalQos`. a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[rabbitAdmin]]<> + (admin) @@ -6232,6 +6283,7 @@ Defaults to a `RabbitAdmin` that declares all non-conditional elements. a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[receiveTimeout]]<> + (receive-timeout) @@ -6243,6 +6295,7 @@ When `consumerBatchEnabled` is true, a partial batch will be delivered if this t a|image::images/tickmark.png[] a| +a| |[[recoveryBackOff]]<> + (recovery-back-off) @@ -6253,6 +6306,7 @@ Mutually exclusive with `recoveryInterval`. a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[recoveryInterval]]<> + (recovery-interval) @@ -6263,6 +6317,7 @@ Mutually exclusive with `recoveryBackOff`. a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[retryDeclarationInterval]]<> + (missing-queue- @@ -6277,6 +6332,7 @@ Default: 60000 (one minute). a|image::images/tickmark.png[] a| +a| |[[shutdownTimeout]]<> + (N/A) @@ -6287,6 +6343,7 @@ Defaults to five seconds. a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[startConsumerMinInterval]]<> + (min-start-interval) @@ -6297,6 +6354,7 @@ Default: 10000 (10 seconds). a|image::images/tickmark.png[] a| +a| |[[statefulRetryFatal]]<> + WithNullMessageId @@ -6308,6 +6366,7 @@ Set this to `false` to discard (or route to a dead-letter queue) such messages. a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[stopConsumerMinInterval]]<> + (min-stop-interval) @@ -6318,6 +6377,16 @@ Default: 60000 (one minute). a|image::images/tickmark.png[] a| +a| + +|[[streamConverter]]<> + +(N/A) + +|A `StreamMessageConverter` to convert a native Stream message to a Spring AMQP message. + +a| +a| +a|image::images/tickmark.png[] |[[taskExecutor]]<> + (task-executor) @@ -6327,6 +6396,7 @@ Default is a `SimpleAsyncTaskExecutor`, using internally managed threads. a|image::images/tickmark.png[] a|image::images/tickmark.png[] +a| |[[taskScheduler]]<> + (task-scheduler) @@ -6335,6 +6405,7 @@ a|image::images/tickmark.png[] a| a|image::images/tickmark.png[] +a| |[[transactionManager]]<> + (transaction-manager) @@ -6344,7 +6415,7 @@ Also complementary to `channelTransacted` -- if the `Channel` is transacted, its a|image::images/tickmark.png[] a|image::images/tickmark.png[] - +a| |=== [[listener-concurrency]] diff --git a/src/reference/asciidoc/index.adoc b/src/reference/asciidoc/index.adoc index 3fdc7429e6..2ca7f6a9d4 100644 --- a/src/reference/asciidoc/index.adoc +++ b/src/reference/asciidoc/index.adoc @@ -46,6 +46,8 @@ This part also includes a chapter about the <>. include::amqp.adoc[] +include::stream.adoc[] + include::logging.adoc[] include::sample-apps.adoc[] diff --git a/src/reference/asciidoc/stream.adoc b/src/reference/asciidoc/stream.adoc new file mode 100644 index 0000000000..11f836d8a0 --- /dev/null +++ b/src/reference/asciidoc/stream.adoc @@ -0,0 +1,138 @@ +[[stream-support]] +=== Using the RabbitMQ Stream Plugin + +Version 2.4 introduces initial support for the https://github.com/rabbitmq/rabbitmq-stream-java-client[RabbitMQ Stream Plugin Java Client] for the https://rabbitmq.com/stream.html[RabbitMQ Stream Plugin]. + +* `RabbitStreamTemplate` +* `StreamListenerContainer` + +==== Sending Messages + +The `RabbitStreamTemplate` provides a subset of the `RabbitTemplate` (AMQP) functionality. + +.RabbitStreamOperations +==== +[source, java] +---- +public interface RabbitStreamOperations extends AutoCloseable { + + ListenableFuture send(Message message); + + ListenableFuture convertAndSend(Object message); + + ListenableFuture convertAndSend(Object message, @Nullable MessagePostProcessor mpp); + + ListenableFuture send(com.rabbitmq.stream.Message message); + + MessageBuilder messageBuilder(); + + MessageConverter messageConverter(); + + StreamMessageConverter streamMessageConverter(); + + @Override + void close() throws AmqpException; + +} +---- +==== + +The `RabbitStreamTemplate` implementation has the following constructor and properties: + +.RabbitStreamTemplate +==== +[source, java] +---- +public RabbitStreamTemplate(Environment environment, String streamName) { +} + +public void setMessageConverter(MessageConverter messageConverter) { +} + +public void setStreamConverter(StreamMessageConverter streamConverter) { +} + +public synchronized void setProducerCustomizer(ProducerCustomizer producerCustomizer) { +} +---- +==== + +The `MessageConverter` is used in the `convertAndSend` methods to convert the object to a Spring AMQP `Message`. + +The `StreamMessageConverter` is used to convert from a Spring AMQP `Message` to a native stream `Message`. + +You can also send native stream `Message` s directly; with the `messageBuilder()` method provding access to the `Producer` 's message builder. + +The `ProducerCustomizer` provides a mechanism to customize the producer before it is built. + +Refer to the https://rabbitmq.github.io/rabbitmq-stream-java-client/stable/htmlsingle/[Java Client Documentation] about customizing the `Environment` and `Producer`. + +==== Receiving Messages + +Asynchronous message reception is provided by the `StreamListenerContainer` (and the `StreamRabbitListenerContainerFactory` when using `@RabbitListener`). + +The listener container requires an `Environment` as well as a single stream name. + +You can either receive Spring AMQP `Message` s using the classic `MessageListener`, or you can receive native stream `Message` s using a new interface: + +==== +[source, java] +---- +public interface StreamMessageListener extends MessageListener { + + void onStreamMessage(Message message, Context context); + +} +---- +==== + +See <> for information about supported properties. + +Similar the template, the container has a `ConsumerCustomizer` property. + +Refer to the https://rabbitmq.github.io/rabbitmq-stream-java-client/stable/htmlsingle/[Java Client Documentation] about customizing the `Environment` and `Consumer`. + +When using `@RabbitListener`, configure a `StreamRabbitListenerContainerFactory`; at this time, most `@RabbitListener` properties (`concurrency`, etc) are ignored. Only `id`, `queues`, `autoStartup` and `containerFactory` are supported. +In addition, `queues` can only contain one stream name. + +==== Examples + +==== +[source, java] +---- +@Bean +RabbitStreamTemplate streamTemplate(Environment env) { + RabbitStreamTemplate template = new RabbitStreamTemplate(env, "test.stream.queue1"); + template.setProducerCustomizer((name, builder) -> builder.name("test")); + return template; +} + +@Bean +RabbitListenerContainerFactory rabbitListenerContainerFactory(Environment env) { + return new StreamRabbitListenerContainerFactory(env); +} + +@RabbitListener(queues = "test.stream.queue1") +void listen(String in) { + ... +} + +@Bean +RabbitListenerContainerFactory nativeFactory(Environment env) { + StreamRabbitListenerContainerFactory factory = new StreamRabbitListenerContainerFactory(env); + factory.setNativeListener(true); + factory.setConsumerCustomizer((id, builder) -> { + builder.name("myConsumer") + .offset(OffsetSpecification.first()) + .manualTrackingStrategy(); + }); + return factory; +} + +@RabbitListener(id = "test", queues = "test.stream.queue2", containerFactory = "nativeFactory") +void nativeMsg(Message in, Context context) { + ... + context.storeOffset(); +} +---- +==== From 42e37cdd673f8bef440dcf4e5e5931fe1a09449c Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Fri, 17 Sep 2021 10:22:53 -0400 Subject: [PATCH 011/737] Upgrade versions; prepare for 2.4.0-M2 --- build.gradle | 18 ++++++++---------- .../listener/AbstractIntegrationTests.java | 3 ++- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/build.gradle b/build.gradle index ae85085e4f..b4013452e5 100644 --- a/build.gradle +++ b/build.gradle @@ -44,27 +44,26 @@ ext { awaitilityVersion = '4.1.0' commonsCompressVersion = '1.20' commonsHttpClientVersion = '4.5.13' - commonsPoolVersion = '2.10.0' + commonsPoolVersion = '2.11.1' googleJsr305Version = '3.0.2' hamcrestVersion = '2.2' hibernateValidationVersion = '6.2.0.Final' - jacksonBomVersion = '2.12.4' + jacksonBomVersion = '2.12.5' jaywayJsonPathVersion = '2.4.0' junit4Version = '4.13.2' junitJupiterVersion = '5.7.2' log4jVersion = '2.14.1' logbackVersion = '1.2.3' lz4Version = '1.8.0' - micrometerVersion = '1.8.0-M2' + micrometerVersion = '1.8.0-M3' mockitoVersion = '3.11.2' - protonJVersion = '0.33.8' - rabbitmqStreamVersion = '0.3.0' - rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.13.0' - rabbitmqHttpClientVersion = '3.11.0' + rabbitmqStreamVersion = '0.4.0' + rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.13.1' + rabbitmqHttpClientVersion = '3.12.1' reactorVersion = '2020.0.11' snappyVersion = '1.1.8.4' - springDataCommonsVersion = '2.6.0-M2' - springVersion = project.hasProperty('springVersion') ? project.springVersion : '5.3.9' + springDataCommonsVersion = '2.6.0-M3' + springVersion = project.hasProperty('springVersion') ? project.springVersion : '5.3.10' springRetryVersion = '1.3.1' zstdJniVersion = '1.5.0-2' } @@ -407,7 +406,6 @@ project('spring-rabbit-stream') { testRuntimeOnly 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' testRuntimeOnly 'com.fasterxml.jackson.module:jackson-module-kotlin' testRuntimeOnly "org.apache.httpcomponents:httpclient:$commonsHttpClientVersion" - testRuntimeOnly "org.apache.qpid:proton-j:$protonJVersion" testRuntimeOnly "org.apache.commons:commons-compress:$commonsCompressVersion" testRuntimeOnly "org.xerial.snappy:snappy-java:$snappyVersion" testRuntimeOnly "org.lz4:lz4-java:$lz4Version" diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/AbstractIntegrationTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/AbstractIntegrationTests.java index 083684c57e..8739017fc2 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/AbstractIntegrationTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/AbstractIntegrationTests.java @@ -31,7 +31,8 @@ public abstract class AbstractIntegrationTests { static final GenericContainer RABBITMQ; static { - if (System.getProperty("spring.rabbit.use.local.server") == null) { + if (System.getProperty("spring.rabbit.use.local.server") == null + && System.getenv("SPRING_RABBIT_USE_LOCAL_SERVER") == null) { String image = "pivotalrabbitmq/rabbitmq-stream"; String cache = System.getenv().get("IMAGE_CACHE"); if (cache != null) { From a67c33d5a1467728c97f0fce394e9f2d7d251911 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Fri, 17 Sep 2021 16:23:03 +0000 Subject: [PATCH 012/737] [artifactory-release] Release version 2.4.0-M3 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 71da3eb0af..b03b24f935 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=2.4.0-SNAPSHOT +version=2.4.0-M3 org.gradlee.caching=true org.gradle.daemon=true org.gradle.parallel=true From 520671bba46e688d2c1ecde77b6b86545206ec04 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Fri, 17 Sep 2021 16:23:06 +0000 Subject: [PATCH 013/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index b03b24f935..71da3eb0af 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=2.4.0-M3 +version=2.4.0-SNAPSHOT org.gradlee.caching=true org.gradle.daemon=true org.gradle.parallel=true From 35b233e6619cefb899444b1c932fd191d4b80391 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 23 Sep 2021 16:13:34 -0400 Subject: [PATCH 014/737] Fix Possible NPE Since maps are built on the fly, they need to support concurrency. --- .../rabbit/listener/adapter/DelegatingInvocableHandler.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandler.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandler.java index 6778848b5a..8d21b14b29 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandler.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandler.java @@ -20,7 +20,6 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -75,7 +74,7 @@ public class DelegatingInvocableHandler { private final InvocableHandlerMethod defaultHandler; - private final Map handlerSendTo = new HashMap<>(); + private final Map handlerSendTo = new ConcurrentHashMap<>(); private final Object bean; From 6628bcbc60d6c826f649b67eea4ca100405bbc7b Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Fri, 24 Sep 2021 09:47:18 -0400 Subject: [PATCH 015/737] GH-1370: Containers: Find Admin in Parent Context Resolves https://github.com/spring-projects/spring-amqp/issues/1370 --- .../AbstractMessageListenerContainer.java | 7 ++- .../rabbit/listener/ContainerAdminTests.java | 55 +++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerAdminTests.java diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index 1483a7cbaa..7a89f2df85 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -71,6 +71,7 @@ import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.DisposableBean; import org.springframework.context.ApplicationContext; @@ -1802,8 +1803,10 @@ protected void updateLastReceive() { } protected void configureAdminIfNeeded() { - if (this.amqpAdmin == null && this.getApplicationContext() != null) { - Map admins = this.getApplicationContext().getBeansOfType(AmqpAdmin.class); + if (this.amqpAdmin == null && this.applicationContext != null) { + Map admins = + BeanFactoryUtils.beansOfTypeIncludingAncestors(this.applicationContext, AmqpAdmin.class, + false, false); if (admins.size() == 1) { this.amqpAdmin = admins.values().iterator().next(); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerAdminTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerAdminTests.java new file mode 100644 index 0000000000..62f478873f --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerAdminTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2021 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; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.junit.RabbitAvailable; +import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; +import org.springframework.amqp.utils.test.TestUtils; +import org.springframework.context.support.GenericApplicationContext; + +/** + * @author Gary Russell + * @since 2.4 + * + */ +@RabbitAvailable +public class ContainerAdminTests { + + @Test + void findAdminInParentContext() { + GenericApplicationContext parent = new GenericApplicationContext(); + CachingConnectionFactory cf = + new CachingConnectionFactory(RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); + RabbitAdmin admin = new RabbitAdmin(cf); + parent.registerBean(RabbitAdmin.class, () -> admin); + parent.refresh(); + GenericApplicationContext child = new GenericApplicationContext(parent); + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(cf); + child.registerBean(SimpleMessageListenerContainer.class, () -> container); + child.refresh(); + container.start(); + assertThat(TestUtils.getPropertyValue(container, "amqpAdmin")).isSameAs(admin); + container.stop(); + } + +} From 9371f8c7d524656bf2df26dd0cc7a1844aef5ad0 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 27 Sep 2021 12:09:38 -0400 Subject: [PATCH 016/737] GH-1732: Fix Listener Container Parser Resolves https://github.com/spring-projects/spring-amqp/issues/1372 Do not split `queue-names` in the parser - leave it to Spring's type converter. Also support SpEL in `queues`; a more elegant solution would require major refactoring of the parser; so this is just a compromise. **cherry-pick to 2.3.x, 2.2.x** --- .../config/ListenerContainerParser.java | 24 +++++++++---------- ...stenerContainerPlaceholderParserTests.java | 19 ++++++++++++++- .../annotation/rabbit-listener.properties | 2 ++ ...ontainerPlaceholderParserTests-context.xml | 10 ++++++++ 4 files changed, 42 insertions(+), 13 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerParser.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerParser.java index d574c87657..0fb64b643c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerParser.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 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. @@ -179,21 +179,21 @@ private void parseListener(Element listenerEle, Element containerEle, ParserCont String queueNames = listenerEle.getAttribute(QUEUE_NAMES_ATTRIBUTE); if (StringUtils.hasText(queueNames)) { - String[] names = StringUtils.commaDelimitedListToStringArray(queueNames); - List values = new ManagedList(); - for (int i = 0; i < names.length; i++) { - values.add(new TypedStringValue(names[i].trim())); - } - containerDef.getPropertyValues().add("queueNames", values); + containerDef.getPropertyValues().add("queueNames", queueNames); } String queues = listenerEle.getAttribute(QUEUES_ATTRIBUTE); if (StringUtils.hasText(queues)) { - String[] names = StringUtils.commaDelimitedListToStringArray(queues); - List values = new ManagedList(); - for (int i = 0; i < names.length; i++) { - values.add(new RuntimeBeanReference(names[i].trim())); + if (queues.startsWith("#{")) { + containerDef.getPropertyValues().add("queues", queues); + } + else { + String[] names = StringUtils.commaDelimitedListToStringArray(queues); + List values = new ManagedList(); + for (int i = 0; i < names.length; i++) { + values.add(new RuntimeBeanReference(names[i].trim())); + } + containerDef.getPropertyValues().add("queues", values); } - containerDef.getPropertyValues().add("queues", values); } ManagedMap args = new ManagedMap(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerPlaceholderParserTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerPlaceholderParserTests.java index 5baf91f756..ebbdd8a5ab 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerPlaceholderParserTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerPlaceholderParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2019 the original author or authors. + * Copyright 2010-2021 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. @@ -82,4 +82,21 @@ public void testParseWithQueueNames() throws Exception { assertThat(Arrays.asList(container.getQueueNames()).toString()).isEqualTo("[foo, " + queue.getName() + "]"); } + @Test + public void commasInPropertyNames() { + SimpleMessageListenerContainer container = this.context.getBean("commaProps1", + SimpleMessageListenerContainer.class); + assertThat(container.getQueueNames()).containsExactly("foo", "bar"); + } + + @Test + public void commasInPropertyQueues() { + SimpleMessageListenerContainer container = this.context.getBean("commaProps2", + SimpleMessageListenerContainer.class); + String[] queueNames = container.getQueueNames(); + assertThat(queueNames).hasSize(2); + assertThat(queueNames[0]).isEqualTo("foo"); + assertThat(queueNames[1]).startsWith("spring.gen"); + } + } diff --git a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/annotation/rabbit-listener.properties b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/annotation/rabbit-listener.properties index 991af5e15b..61aa68f57f 100644 --- a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/annotation/rabbit-listener.properties +++ b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/annotation/rabbit-listener.properties @@ -4,3 +4,5 @@ rabbit.listener.queue=queue1 rabbit.listener.priority=34 rabbit.listener.responseRoutingKey=routing-123 rabbit.listener.admin=rabbitAdmin + +foo.and.bar=foo, bar diff --git a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/ListenerContainerPlaceholderParserTests-context.xml b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/ListenerContainerPlaceholderParserTests-context.xml index 35c7c46591..725bdb42c8 100644 --- a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/ListenerContainerPlaceholderParserTests-context.xml +++ b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/ListenerContainerPlaceholderParserTests-context.xml @@ -11,6 +11,7 @@ 5 1 false + foo, bar @@ -28,4 +29,13 @@ + + + + + + + + From ed824637cc922356b44409827722c4628340b7c8 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 27 Sep 2021 14:00:54 -0400 Subject: [PATCH 017/737] Do not deserialize in Message.toString() --- .../springframework/amqp/core/Message.java | 24 ++++--------------- .../amqp/core/MessageTests.java | 5 ++-- 2 files changed, 6 insertions(+), 23 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/Message.java b/spring-amqp/src/main/java/org/springframework/amqp/core/Message.java index 15e9c6c727..f10cc5d7a3 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/Message.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/Message.java @@ -16,16 +16,11 @@ package org.springframework.amqp.core; -import java.io.ByteArrayInputStream; import java.io.Serializable; import java.nio.charset.Charset; import java.util.Arrays; -import java.util.LinkedHashSet; -import java.util.Set; -import org.springframework.amqp.utils.SerializationUtils; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; /** * The 0-8 and 0-9-1 AMQP specifications do not define an Message class or interface. Instead, when performing an @@ -48,9 +43,6 @@ public class Message implements Serializable { private static final String DEFAULT_ENCODING = Charset.defaultCharset().name(); - private static final Set ALLOWED_LIST_PATTERNS = - new LinkedHashSet<>(Arrays.asList("java.util.*", "java.lang.*")); - private static String bodyEncoding = DEFAULT_ENCODING; private final MessageProperties messageProperties; @@ -79,20 +71,13 @@ public Message(byte[] body, MessageProperties messageProperties) { //NOSONAR } /** - * Add patterns to the allowed list of permissible package/class name patterns for - * deserialization in {@link #toString()}. - * The patterns will be applied in order until a match is found. - * A class can be fully qualified or a wildcard '*' is allowed at the - * beginning or end of the class name. - * Examples: {@code com.foo.*}, {@code *.MyClass}. - * By default, only {@code java.util} and {@code java.lang} classes will be - * deserialized. + * No longer used. + * @deprecated toString() no longer deserializes the body. * @param patterns the patterns. * @since 1.5.7 */ + @Deprecated public static void addAllowedListPatterns(String... patterns) { - Assert.notNull(patterns, "'patterns' cannot be null"); - ALLOWED_LIST_PATTERNS.addAll(Arrays.asList(patterns)); } /** @@ -128,8 +113,7 @@ private String getBodyContentAsString() { try { String contentType = this.messageProperties.getContentType(); if (MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT.equals(contentType)) { - return SerializationUtils.deserialize(new ByteArrayInputStream(this.body), ALLOWED_LIST_PATTERNS, - ClassUtils.getDefaultClassLoader()).toString(); + return "[serialized object]"; } String encoding = encoding(); if (MessageProperties.CONTENT_TYPE_TEXT_PLAIN.equals(contentType) diff --git a/spring-amqp/src/test/java/org/springframework/amqp/core/MessageTests.java b/spring-amqp/src/test/java/org/springframework/amqp/core/MessageTests.java index b9eb8b8bd9..045e3332f0 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/core/MessageTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/core/MessageTests.java @@ -106,9 +106,8 @@ public void fooNotDeserialized() { Message listMessage = new SimpleMessageConverter().toMessage(Collections.singletonList(new Foo()), new MessageProperties()); assertThat(listMessage.toString()).doesNotContainPattern("aFoo"); - Message.addAllowedListPatterns(Foo.class.getName()); - assertThat(message.toString()).contains("aFoo"); - assertThat(listMessage.toString()).contains("aFoo"); + assertThat(message.toString()).contains("[serialized object]"); + assertThat(listMessage.toString()).contains("[serialized object]"); } @SuppressWarnings("serial") From bb06e92a81248ba03ab96b7b45dfa29a911c0b03 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 27 Sep 2021 15:46:52 -0400 Subject: [PATCH 018/737] Upgrade checkstyle; fix violations - a checkstyle bug prevented detection of javadoc tag ordering - `allowMissingJavadoc` is no longer available on `JavadocMethod` (replaced by `MissingJavadocMethod`) --- build.gradle | 2 +- .../src/main/java/org/springframework/amqp/core/Message.java | 2 +- .../amqp/support/converter/DefaultJackson2JavaTypeMapper.java | 4 ++-- .../test/mockito/LatchCountDownAndCallRealMethodAnswer.java | 4 ++-- .../amqp/rabbit/connection/AbstractConnectionFactory.java | 2 +- .../amqp/rabbit/connection/CorrelationData.java | 4 ++-- src/checkstyle/checkstyle.xml | 3 --- 7 files changed, 9 insertions(+), 12 deletions(-) diff --git a/build.gradle b/build.gradle index b4013452e5..56e6ad8acf 100644 --- a/build.gradle +++ b/build.gradle @@ -288,7 +288,7 @@ subprojects { subproject -> checkstyle { configDirectory.set(rootProject.file("src/checkstyle")) - toolVersion = '8.24' + toolVersion = '9.0' } jar { diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/Message.java b/spring-amqp/src/main/java/org/springframework/amqp/core/Message.java index f10cc5d7a3..1690227f62 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/Message.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/Message.java @@ -72,9 +72,9 @@ public Message(byte[] body, MessageProperties messageProperties) { //NOSONAR /** * No longer used. - * @deprecated toString() no longer deserializes the body. * @param patterns the patterns. * @since 1.5.7 + * @deprecated toString() no longer deserializes the body. */ @Deprecated public static void addAllowedListPatterns(String... patterns) { diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapper.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapper.java index a8aea4f702..ed3423efa8 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapper.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -52,8 +52,8 @@ public class DefaultJackson2JavaTypeMapper extends AbstractJavaTypeMapper implem /** * Return the precedence. * @return the precedence. - * @see #setTypePrecedence(Jackson2JavaTypeMapper.TypePrecedence) * @since 1.6. + * @see #setTypePrecedence(Jackson2JavaTypeMapper.TypePrecedence) */ @Override public TypePrecedence getTypePrecedence() { diff --git a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LatchCountDownAndCallRealMethodAnswer.java b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LatchCountDownAndCallRealMethodAnswer.java index fac417b2a9..ae42862f4f 100644 --- a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LatchCountDownAndCallRealMethodAnswer.java +++ b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LatchCountDownAndCallRealMethodAnswer.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2021 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. @@ -48,9 +48,9 @@ public class LatchCountDownAndCallRealMethodAnswer extends ForwardsInvocations { /** * Get an instance with no delegate. + * @param count to set in a {@link CountDownLatch}. * @deprecated in favor of * {@link #LatchCountDownAndCallRealMethodAnswer(int, Object)}. - * @param count to set in a {@link CountDownLatch}. */ @Deprecated public LatchCountDownAndCallRealMethodAnswer(int count) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java index 95fb13ba56..db56a8d7bb 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java @@ -529,8 +529,8 @@ protected String getBeanName() { * connection to the broker will be attempted in random order. * @param shuffleAddresses true to shuffle the list. * @since 2.1.8 - * @see Collections#shuffle(List) * @deprecated since 2.3 in favor of + * @see Collections#shuffle(List) * {@link #setAddressShuffleMode(AddressShuffleMode)}. */ @Deprecated diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java index d921a24ae9..14162a82d8 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java @@ -99,9 +99,9 @@ public SettableListenableFuture getFuture() { * Return a returned message, if any; requires a unique * {@link #CorrelationData(String) id}. Guaranteed to be populated before the future * is set. - * @deprecated in favor of {@link #getReturned()}. * @return the message or null. * @since 2.1 + * @deprecated in favor of {@link #getReturned()}. */ @Deprecated @Nullable @@ -117,8 +117,8 @@ public Message getReturnedMessage() { /** * Set a returned message for this correlation data. * @param returnedMessage the returned message. - * @deprecated in favor of {@link #setReturned(ReturnedMessage)}. * @since 1.7.13 + * @deprecated in favor of {@link #setReturned(ReturnedMessage)}. */ @Deprecated public void setReturnedMessage(Message returnedMessage) { diff --git a/src/checkstyle/checkstyle.xml b/src/checkstyle/checkstyle.xml index 92584d739d..ff50870bf9 100644 --- a/src/checkstyle/checkstyle.xml +++ b/src/checkstyle/checkstyle.xml @@ -100,9 +100,6 @@ - - - From c2530c5d8f91ef5668b45c5c8b2becdb6ebc046e Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 12 Oct 2021 10:31:43 -0400 Subject: [PATCH 019/737] Remove Incorrect Doc Re Prefetch with MANUAL Acks This commit https://github.com/spring-projects/spring-amqp/commit/b945d1cc0216fc1a8a8af8c1f8f5a7d234d2543e incorrectly added text about reducing the prefetch to 1 when using manual acks. While changing it to 1 fixed a test case (`testListenerRecoversFromBogusDoubleAck`) it should not have been a general recommendation since acking the same message twice is not a valid situation. **cherry-pick to all supported branches** --- src/reference/asciidoc/amqp.adoc | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index 4d1fbcbee3..07b329234a 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -1918,8 +1918,6 @@ There are, nevertheless, scenarios where the prefetch value should be low: * 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. -We also recommend using `prefetch = 1` with the `MANUAL` `ack` mode. -The `basicAck` is an asynchronous operation and, if something wrong happens on the Broker (double `ack` for the same delivery tag, for example), you end up with processed subsequent messages in the batch that are unacknowledged on the Broker, and other consumers may see them. See <>. From bb9d6ab6052b206af3991bfc8a0cc43b68aa3a19 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 18 Oct 2021 11:15:25 -0400 Subject: [PATCH 020/737] Upgrade versions; prepare for release --- build.gradle | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 56e6ad8acf..076456f576 100644 --- a/build.gradle +++ b/build.gradle @@ -48,22 +48,22 @@ ext { googleJsr305Version = '3.0.2' hamcrestVersion = '2.2' hibernateValidationVersion = '6.2.0.Final' - jacksonBomVersion = '2.12.5' + jacksonBomVersion = '2.13.0' jaywayJsonPathVersion = '2.4.0' junit4Version = '4.13.2' junitJupiterVersion = '5.7.2' log4jVersion = '2.14.1' logbackVersion = '1.2.3' lz4Version = '1.8.0' - micrometerVersion = '1.8.0-M3' + micrometerVersion = '1.8.0-RC1' mockitoVersion = '3.11.2' rabbitmqStreamVersion = '0.4.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.13.1' rabbitmqHttpClientVersion = '3.12.1' - reactorVersion = '2020.0.11' + reactorVersion = '2020.0.12' snappyVersion = '1.1.8.4' - springDataCommonsVersion = '2.6.0-M3' - springVersion = project.hasProperty('springVersion') ? project.springVersion : '5.3.10' + springDataCommonsVersion = '2.6.0-RC1' + springVersion = project.hasProperty('springVersion') ? project.springVersion : '5.3.11' springRetryVersion = '1.3.1' zstdJniVersion = '1.5.0-2' } From 6be75ad3ac2955463ef1577c667e8485e889e9e6 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 18 Oct 2021 16:33:51 +0000 Subject: [PATCH 021/737] [artifactory-release] Release version 2.4.0-RC1 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 71da3eb0af..ded02c753a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=2.4.0-SNAPSHOT +version=2.4.0-RC1 org.gradlee.caching=true org.gradle.daemon=true org.gradle.parallel=true From 0a9612440b3664e60aac3e7ba3cd028dee12d6a0 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 18 Oct 2021 16:33:53 +0000 Subject: [PATCH 022/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index ded02c753a..71da3eb0af 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=2.4.0-RC1 +version=2.4.0-SNAPSHOT org.gradlee.caching=true org.gradle.daemon=true org.gradle.parallel=true From 52ae483d6c75106e4c8681c9a4930033f181bd8f Mon Sep 17 00:00:00 2001 From: Jay Bryant Date: Wed, 20 Oct 2021 10:59:26 -0500 Subject: [PATCH 023/737] Update asciidoctor plugin to 3.3.2 --- build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 076456f576..8a24ca112d 100644 --- a/build.gradle +++ b/build.gradle @@ -20,9 +20,9 @@ plugins { id 'io.spring.nohttp' version '0.0.4.RELEASE' id 'io.spring.dependency-management' version '1.0.9.RELEASE' apply false id 'com.jfrog.artifactory' version '4.13.0' apply false - id 'org.asciidoctor.jvm.pdf' version '3.1.0' - id 'org.asciidoctor.jvm.gems' version '3.1.0' - id 'org.asciidoctor.jvm.convert' version '3.1.0' + id 'org.asciidoctor.jvm.pdf' version '3.3.2' + id 'org.asciidoctor.jvm.gems' version '3.3.2' + id 'org.asciidoctor.jvm.convert' version '3.3.2' } description = 'Spring AMQP' From 18d58b5ef3fb93019c7d00e2983052a97b55a71f Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 4 Nov 2021 15:14:41 -0400 Subject: [PATCH 024/737] Upgrade jfrog-cli for Central Action --- .github/workflows/central-sync-create.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/central-sync-create.yml b/.github/workflows/central-sync-create.yml index 2d31cdcaa7..5f7f0e7df2 100644 --- a/.github/workflows/central-sync-create.yml +++ b/.github/workflows/central-sync-create.yml @@ -21,7 +21,7 @@ jobs: # Setup jfrog cli - uses: jfrog/setup-jfrog-cli@v1 with: - version: 1.43.2 + version: 1.51.1 env: JF_ARTIFACTORY_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} From 1f6225b73ecf038d32d797f1be5413c559a67ab3 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 9 Nov 2021 15:48:13 -0500 Subject: [PATCH 025/737] GH-1383: Deprecate Remoting Resolves https://github.com/spring-projects/spring-amqp/issues/1383 --- .../amqp/remoting/client/AmqpClientInterceptor.java | 2 ++ .../amqp/remoting/client/AmqpProxyFactoryBean.java | 2 ++ .../remoting/service/AmqpInvokerServiceExporter.java | 2 ++ .../springframework/amqp/remoting/RemotingTest.java | 10 +++++----- src/reference/asciidoc/amqp.adoc | 4 ++++ src/reference/asciidoc/whats-new.adoc | 5 +++++ 6 files changed, 20 insertions(+), 5 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/remoting/client/AmqpClientInterceptor.java b/spring-amqp/src/main/java/org/springframework/amqp/remoting/client/AmqpClientInterceptor.java index 0537060730..6aeaa3177e 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/remoting/client/AmqpClientInterceptor.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/remoting/client/AmqpClientInterceptor.java @@ -38,7 +38,9 @@ * @see org.springframework.amqp.remoting.service.AmqpInvokerServiceExporter * @see AmqpProxyFactoryBean * @see org.springframework.remoting.RemoteAccessException + * @deprecated will be removed in 3.0.0. */ +@Deprecated public class AmqpClientInterceptor extends RemoteAccessor implements MethodInterceptor { private AmqpTemplate amqpTemplate; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/remoting/client/AmqpProxyFactoryBean.java b/spring-amqp/src/main/java/org/springframework/amqp/remoting/client/AmqpProxyFactoryBean.java index d63ddce60e..4ce946f470 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/remoting/client/AmqpProxyFactoryBean.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/remoting/client/AmqpProxyFactoryBean.java @@ -40,7 +40,9 @@ * @see AmqpClientInterceptor * @see org.springframework.remoting.rmi.RmiServiceExporter * @see org.springframework.remoting.RemoteAccessException + * @deprecated will be removed in 3.0.0. */ +@Deprecated public class AmqpProxyFactoryBean extends AmqpClientInterceptor implements FactoryBean, InitializingBean { private Object serviceProxy; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/remoting/service/AmqpInvokerServiceExporter.java b/spring-amqp/src/main/java/org/springframework/amqp/remoting/service/AmqpInvokerServiceExporter.java index ca14839e87..13817fde02 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/remoting/service/AmqpInvokerServiceExporter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/remoting/service/AmqpInvokerServiceExporter.java @@ -53,7 +53,9 @@ * @author Gary Russell * @author Artem Bilan * @since 1.2 + * @deprecated will be removed in 3.0.0. */ +@Deprecated public class AmqpInvokerServiceExporter extends RemoteInvocationBasedExporter implements MessageListener { private AmqpTemplate amqpTemplate; diff --git a/spring-amqp/src/test/java/org/springframework/amqp/remoting/RemotingTest.java b/spring-amqp/src/test/java/org/springframework/amqp/remoting/RemotingTest.java index fc1acdf210..bf46a25092 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/remoting/RemotingTest.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/remoting/RemotingTest.java @@ -29,8 +29,6 @@ import org.springframework.amqp.core.AmqpTemplate; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; -import org.springframework.amqp.remoting.client.AmqpProxyFactoryBean; -import org.springframework.amqp.remoting.service.AmqpInvokerServiceExporter; import org.springframework.amqp.remoting.testhelper.AbstractAmqpTemplate; import org.springframework.amqp.remoting.testhelper.SentSavingTemplate; import org.springframework.amqp.remoting.testservice.GeneralException; @@ -49,11 +47,12 @@ * @author Gary Russell * @since 1.2 */ +@SuppressWarnings("deprecation") public class RemotingTest { private TestServiceInterface riggedProxy; - private AmqpInvokerServiceExporter serviceExporter; + private org.springframework.amqp.remoting.service.AmqpInvokerServiceExporter serviceExporter; /** * Set up a rig of directly wired-up proxy and service listener so that both can be tested together without needing @@ -63,14 +62,15 @@ public class RemotingTest { public void initializeTestRig() { // Set up the service TestServiceInterface testService = new TestServiceImpl(); - this.serviceExporter = new AmqpInvokerServiceExporter(); + this.serviceExporter = new org.springframework.amqp.remoting.service.AmqpInvokerServiceExporter(); final SentSavingTemplate sentSavingTemplate = new SentSavingTemplate(); this.serviceExporter.setAmqpTemplate(sentSavingTemplate); this.serviceExporter.setService(testService); this.serviceExporter.setServiceInterface(TestServiceInterface.class); // Set up the client - AmqpProxyFactoryBean amqpProxyFactoryBean = new AmqpProxyFactoryBean(); + org.springframework.amqp.remoting.client.AmqpProxyFactoryBean amqpProxyFactoryBean = + new org.springframework.amqp.remoting.client.AmqpProxyFactoryBean(); amqpProxyFactoryBean.setServiceInterface(TestServiceInterface.class); AmqpTemplate directForwardingTemplate = new AbstractAmqpTemplate() { @Override diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index 07b329234a..a471d8e36e 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -4581,6 +4581,10 @@ See <> for more information. [[remoting]] ===== Spring Remoting with AMQP +IMPORTANT: This feature is deprecated and will be removed in 3.0. +It has been superseded for a long time by <> with the `returnExceptions` being set to true, and configuring a `RemoteInvocationAwareMessageConverterAdapter` on the sending side. +See <> for more information. + The Spring Framework has a general remoting capability, allowing https://docs.spring.io/spring/docs/current/spring-framework-reference/html/remoting.html[Remote Procedure Calls (RPC)] that use various transports. Spring-AMQP supports a similar mechanism with a `AmqpProxyFactoryBean` on the client and a `AmqpInvokerServiceExporter` on the server. This provides RPC over AMQP. diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index ba34b2f057..5e6673f42a 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -15,3 +15,8 @@ See <> for more information. A new property `recoverManualDeclarations` allows recovery of manually declared queues/exchanges/bindings. See <> for more information. + +==== Remoting Support + +Support remoting using Spring Framework's RMI support is deprecated and will be removed in 3.0. +See <> for more information. From 430f0bf07d00c4ef7acab0c989881a0d85b64cb3 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 10 Nov 2021 11:28:33 -0500 Subject: [PATCH 026/737] GH-1099: Fix Javadocs for Publisher Connection Resolves https://github.com/spring-projects/spring-amqp/issues/1099 **cherry-pick to 2.3.x, 2.2.x** --- .../amqp/rabbit/core/RabbitTemplate.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java index 900d0b0b4b..652c0c634a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java @@ -823,10 +823,12 @@ public boolean isUsePublisherConnection() { } /** - * To avoid deadlocked connections, it is generally recommended to use - * a separate connection for publishers and consumers (except when a publisher - * is participating in a consumer transaction). Default 'false'; will change - * to 'true' in 2.1. + * To avoid deadlocked connections, it is generally recommended to use a separate + * connection for publishers and consumers (except when a publisher is participating + * in a consumer transaction). Default 'false'. When setting this to true, be careful + * in that a {@link RabbitAdmin} that uses this template will declare queues on the + * publisher connection; this may not be what you expect, especially with exclusive + * queues that might be consumed in this application. * @param usePublisherConnection true to use a publisher connection. * @since 2.0.2 */ From da5fa575bc987e5be2c057eb861b1359cce27b27 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 10 Nov 2021 12:44:28 -0500 Subject: [PATCH 027/737] Fix Link in Issue Template --- .github/ISSUE_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 2725e32176..58356963ad 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,5 +1,5 @@ **Affects Version(s):** \ From 4f3a1539da2dddb5f179b53cbb7f46e89a45632e Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 10 Nov 2021 13:26:00 -0500 Subject: [PATCH 028/737] Message toString() Improvement Don't convert large message bodies to a `String` in `toString()`. Set a limit (50) that can be modified by users. Avoid possible OOM Errors. **cherry-pick to 2.3.x, 2.2.x** --- .../springframework/amqp/core/Message.java | 19 +++++++++++++++++-- .../amqp/core/MessageTests.java | 19 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/Message.java b/spring-amqp/src/main/java/org/springframework/amqp/core/Message.java index 1690227f62..24193ab483 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/Message.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/Message.java @@ -43,8 +43,12 @@ public class Message implements Serializable { private static final String DEFAULT_ENCODING = Charset.defaultCharset().name(); + private static final int DEFAULT_MAX_BODY_LENGTH = 50; + private static String bodyEncoding = DEFAULT_ENCODING; + private static int maxBodyLength = DEFAULT_MAX_BODY_LENGTH; + private final MessageProperties messageProperties; private final byte[] body; @@ -91,6 +95,16 @@ public static void setDefaultEncoding(String encoding) { bodyEncoding = encoding; } + /** + * Set the maximum length of a test message body to render as a String in + * {@link #toString()}. Default 50. + * @param length the length to render. + * @since 2.2.20 + */ + public static void setMaxBodyLength(int length) { + maxBodyLength = length; + } + public byte[] getBody() { return this.body; //NOSONAR } @@ -116,10 +130,11 @@ private String getBodyContentAsString() { return "[serialized object]"; } String encoding = encoding(); - if (MessageProperties.CONTENT_TYPE_TEXT_PLAIN.equals(contentType) + if (this.body.length <= maxBodyLength + && (MessageProperties.CONTENT_TYPE_TEXT_PLAIN.equals(contentType) || MessageProperties.CONTENT_TYPE_JSON.equals(contentType) || MessageProperties.CONTENT_TYPE_JSON_ALT.equals(contentType) - || MessageProperties.CONTENT_TYPE_XML.equals(contentType)) { + || MessageProperties.CONTENT_TYPE_XML.equals(contentType))) { return new String(this.body, encoding); } } diff --git a/spring-amqp/src/test/java/org/springframework/amqp/core/MessageTests.java b/spring-amqp/src/test/java/org/springframework/amqp/core/MessageTests.java index 045e3332f0..4432d7f3e2 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/core/MessageTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/core/MessageTests.java @@ -26,6 +26,7 @@ import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.Date; +import java.util.stream.IntStream; import org.junit.jupiter.api.Test; @@ -110,6 +111,24 @@ public void fooNotDeserialized() { assertThat(listMessage.toString()).contains("[serialized object]"); } + @Test + void dontToStringLongBody() { + MessageProperties messageProperties = new MessageProperties(); + messageProperties.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN); + StringBuilder builder1 = new StringBuilder(); + IntStream.range(0, 50).forEach(i -> builder1.append("x")); + String bodyAsString = builder1.toString(); + Message message = new Message(bodyAsString.getBytes(), messageProperties); + assertThat(message.toString()).contains(bodyAsString); + StringBuilder builder2 = new StringBuilder(); + IntStream.range(0, 51).forEach(i -> builder2.append("x")); + bodyAsString = builder2.toString(); + message = new Message(bodyAsString.getBytes(), messageProperties); + assertThat(message.toString()).contains("[51]"); + Message.setMaxBodyLength(100); + assertThat(message.toString()).contains(bodyAsString); + } + @SuppressWarnings("serial") public static class Foo implements Serializable { From fa1aa1c1115be232796c5eb116131aa9f5ca5cda Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 11 Nov 2021 10:02:53 -0500 Subject: [PATCH 029/737] Fix DEBUG Logging for Projection Don't Log Converted Message With Projection `toString()` cannot be invoked on the payload proxy created by the projection factory. Increase memory for builds. * Unlike Kafka, we can't use the converter type to determine if projection was used. * Add null check. * Remove inadvertently pushed file. --- build.gradle | 3 +++ gradle.properties | 1 + .../amqp/core/MessageProperties.java | 22 +++++++++++++++++++ .../AbstractJackson2MessageConverter.java | 1 + .../MessagingMessageListenerAdapter.java | 6 ++++- 5 files changed, 32 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 8a24ca112d..83a3e51e44 100644 --- a/build.gradle +++ b/build.gradle @@ -242,6 +242,9 @@ subprojects { subproject -> compileKotlin.dependsOn updateCopyrights test { + maxHeapSize = '2g' + jvmArgs '-XX:+HeapDumpOnOutOfMemoryError' + testLogging { events "skipped", "failed" showStandardStreams = project.hasProperty("showStandardStreams") ?: false diff --git a/gradle.properties b/gradle.properties index 71da3eb0af..5234de468b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,5 @@ version=2.4.0-SNAPSHOT +org.gradle.jvmargs=-Xms512m -Xmx4g -XX:MaxPermSize=1024m -XX:MaxMetaspaceSize=1g org.gradlee.caching=true org.gradle.daemon=true org.gradle.parallel=true diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java index 6d61db4ab3..42e92054ca 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java @@ -128,6 +128,8 @@ public class MessageProperties implements Serializable { private boolean lastInBatch; + private boolean projectionUsed; + private transient Type inferredArgumentType; private transient Method targetMethod; @@ -552,6 +554,26 @@ public void setLastInBatch(boolean lastInBatch) { this.lastInBatch = lastInBatch; } + /** + * Get an internal flag used to communicate that conversion used projection; always + * false at the application level. + * @return true if projection was used. + * @since 2.2.20 + */ + public boolean isProjectionUsed() { + return this.projectionUsed; + } + + /** + * Set an internal flag used to communicate that conversion used projection; always false + * at the application level. + * @param projectionUsed true for projection. + * @since 2.2.20 + */ + public void setProjectionUsed(boolean projectionUsed) { + this.projectionUsed = projectionUsed; + } + /** * Return the x-death header. * @return the header. diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java index d0f1460d82..8f3e11315e 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java @@ -305,6 +305,7 @@ private Object convertContent(Message message, Object conversionHint, MessagePro if (inferredType != null && this.useProjectionForInterfaces && inferredType.isInterface() && !inferredType.getRawClass().getPackage().getName().startsWith("java.util")) { // List etc content = this.projectingConverter.convert(message, inferredType.getRawClass()); + properties.setProjectionUsed(true); } else if (inferredType != null && this.alwaysConvertToInferredType) { content = tryConverType(message, encoding, inferredType); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java index 324bfc32b7..ce4b30a416 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java @@ -187,7 +187,11 @@ private void handleException(org.springframework.amqp.core.Message amqpMessage, protected void invokeHandlerAndProcessResult(@Nullable org.springframework.amqp.core.Message amqpMessage, Channel channel, Message message) throws Exception { // NOSONAR - if (logger.isDebugEnabled()) { + boolean projectionUsed = amqpMessage == null ? false : amqpMessage.getMessageProperties().isProjectionUsed(); + if (projectionUsed) { + amqpMessage.getMessageProperties().setProjectionUsed(false); + } + if (logger.isDebugEnabled() && !projectionUsed) { logger.debug("Processing [" + message + "]"); } InvocationResult result = null; From 84ff11327edf481fcb71a57761e21f2d64653a82 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 15 Nov 2021 09:11:43 -0500 Subject: [PATCH 030/737] Fix Sonar Issue --- .../src/main/java/org/springframework/amqp/core/Message.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/Message.java b/spring-amqp/src/main/java/org/springframework/amqp/core/Message.java index 24193ab483..32682fb950 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/Message.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/Message.java @@ -130,7 +130,7 @@ private String getBodyContentAsString() { return "[serialized object]"; } String encoding = encoding(); - if (this.body.length <= maxBodyLength + if (this.body.length <= maxBodyLength // NOSONAR && (MessageProperties.CONTENT_TYPE_TEXT_PLAIN.equals(contentType) || MessageProperties.CONTENT_TYPE_JSON.equals(contentType) || MessageProperties.CONTENT_TYPE_JSON_ALT.equals(contentType) From d706f4454b8812e7177c16144a3892c0251b99eb Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 15 Nov 2021 11:05:28 -0500 Subject: [PATCH 031/737] Upgrade versions; prepare for release --- build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 83a3e51e44..fe3877c7ff 100644 --- a/build.gradle +++ b/build.gradle @@ -55,15 +55,15 @@ ext { log4jVersion = '2.14.1' logbackVersion = '1.2.3' lz4Version = '1.8.0' - micrometerVersion = '1.8.0-RC1' + micrometerVersion = '1.8.0' mockitoVersion = '3.11.2' rabbitmqStreamVersion = '0.4.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.13.1' rabbitmqHttpClientVersion = '3.12.1' - reactorVersion = '2020.0.12' + reactorVersion = '2020.0.13' snappyVersion = '1.1.8.4' - springDataCommonsVersion = '2.6.0-RC1' - springVersion = project.hasProperty('springVersion') ? project.springVersion : '5.3.11' + springDataCommonsVersion = '2.6.0' + springVersion = project.hasProperty('springVersion') ? project.springVersion : '5.3.13' springRetryVersion = '1.3.1' zstdJniVersion = '1.5.0-2' } From 6235c7b3f7c1c2b6e4caa3b7ff65c6f383703432 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 15 Nov 2021 16:35:34 +0000 Subject: [PATCH 032/737] [artifactory-release] Release version 2.4.0 --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 5234de468b..2fa9a4d064 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ -version=2.4.0-SNAPSHOT -org.gradle.jvmargs=-Xms512m -Xmx4g -XX:MaxPermSize=1024m -XX:MaxMetaspaceSize=1g +version=2.4.0 org.gradlee.caching=true +org.gradle.jvmargs=-Xms512m -Xmx4g -XX:MaxPermSize=1024m -XX:MaxMetaspaceSize=1g org.gradle.daemon=true org.gradle.parallel=true kotlin.stdlib.default.dependency=false From 312d6a385fb3bac70984627dc8ec6f5fc0791b1f Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 15 Nov 2021 16:35:36 +0000 Subject: [PATCH 033/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 2fa9a4d064..1415a98f92 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=2.4.0 +version=2.4.1-SNAPSHOT org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g -XX:MaxPermSize=1024m -XX:MaxMetaspaceSize=1g org.gradle.daemon=true From 57596c6a26be2697273cd97912049b92e81d3f1a Mon Sep 17 00:00:00 2001 From: EddieChoCho Date: Mon, 22 Nov 2021 20:46:59 +0800 Subject: [PATCH 034/737] Add ReturnsCallback XML Configuration --- .../amqp/rabbit/config/TemplateParser.java | 5 +++- .../amqp/rabbit/config/spring-rabbit.xsd | 24 +++++++++++++++---- .../config/TemplateParserTests-context.xml | 4 ++-- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/TemplateParser.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/TemplateParser.java index 8dea913ba8..dc8a6f9b2c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/TemplateParser.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/TemplateParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 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. @@ -67,6 +67,8 @@ class TemplateParser extends AbstractSingleBeanDefinitionParser { private static final String RETURN_CALLBACK_ATTRIBUTE = "return-callback"; + private static final String RETURNS_CALLBACK_ATTRIBUTE = "returns-callback"; + private static final String CONFIRM_CALLBACK_ATTRIBUTE = "confirm-callback"; private static final String CORRELATION_KEY = "correlation-key"; @@ -122,6 +124,7 @@ protected void doParse(Element element, ParserContext parserContext, BeanDefinit NamespaceUtils.setValueIfAttributeDefined(builder, element, USE_TEMPORARY_REPLY_QUEUES_ATTRIBUTE); NamespaceUtils.setValueIfAttributeDefined(builder, element, REPLY_ADDRESS_ATTRIBUTE); NamespaceUtils.setReferenceIfAttributeDefined(builder, element, RETURN_CALLBACK_ATTRIBUTE); + NamespaceUtils.setReferenceIfAttributeDefined(builder, element, RETURNS_CALLBACK_ATTRIBUTE); NamespaceUtils.setReferenceIfAttributeDefined(builder, element, CONFIRM_CALLBACK_ATTRIBUTE); NamespaceUtils.setValueIfAttributeDefined(builder, element, CORRELATION_KEY); NamespaceUtils.setReferenceIfAttributeDefined(builder, element, RETRY_TEMPLATE); diff --git a/spring-rabbit/src/main/resources/org/springframework/amqp/rabbit/config/spring-rabbit.xsd b/spring-rabbit/src/main/resources/org/springframework/amqp/rabbit/config/spring-rabbit.xsd index 09bdec3bbb..7f351ed349 100644 --- a/spring-rabbit/src/main/resources/org/springframework/amqp/rabbit/config/spring-rabbit.xsd +++ b/spring-rabbit/src/main/resources/org/springframework/amqp/rabbit/config/spring-rabbit.xsd @@ -1285,7 +1285,7 @@ @@ -1299,7 +1299,7 @@ A SpEL expression to be evaluated against each request message to determine a 'mandatory' boolean value. The BeanFactoryResolver is available too, if the RabbitTemplate is used from Spring Context, allowing for expressions such as '@myBean.isMandatory(#root)`. - Only applies if a 'return-callback' is provided. Mutually exclusive with 'mandatory'. + Only applies if a 'returns-callback' is provided. Mutually exclusive with 'mandatory'. ]]> @@ -1309,10 +1309,26 @@ A reference to an implementation of RabbitTemplate.ReturnCallback - invoked if a return is received for a message published with mandatory set that couldn't be delivered according to the semantics of that option. + DEPRECTATED - use 'returns-callback' instead. ]]> - + + + + + + + + + + + @@ -1326,7 +1342,7 @@ ]]> - + diff --git a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/TemplateParserTests-context.xml b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/TemplateParserTests-context.xml index c031ae70c7..cbc799a662 100644 --- a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/TemplateParserTests-context.xml +++ b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/TemplateParserTests-context.xml @@ -34,7 +34,7 @@ + mandatory="true" returns-callback="rcb" confirm-callback="ccb"/> - + From a7fd7154799bc65b336ea17fe86a0fb7696d3f74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Michael?= Date: Tue, 30 Nov 2021 15:52:49 +0100 Subject: [PATCH 035/737] GH-1397: Fix typo in @EnableRabbit class javadoc Resolves https://github.com/spring-projects/spring-amqp/issues/1397 --- .../springframework/amqp/rabbit/annotation/EnableRabbit.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/EnableRabbit.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/EnableRabbit.java index f7724f48d4..b22cc737d9 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/EnableRabbit.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/EnableRabbit.java @@ -175,7 +175,7 @@ * @Override * public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) { * registrar.setEndpointRegistry(myRabbitListenerEndpointRegistry()); - * registrar.setMessageHandlerMethodFactory(myMessageHandlerMethodFactory); + * registrar.setMessageHandlerMethodFactory(myMessageHandlerMethodFactory()); * } * * @Bean From aeabc562ef6cc59e4cfc08c8361ef3aa9da64b37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Michael?= Date: Thu, 2 Dec 2021 04:24:21 +0100 Subject: [PATCH 036/737] GH-1396: Declarables constructor is too strict Resolves https://github.com/spring-projects/spring-amqp/issues/1396 * Make constructor more flexible by upper bounded wildcard * Review amendments --- .../amqp/core/Declarables.java | 8 +-- .../amqp/core/DeclarablesTests.java | 55 +++++++++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 spring-amqp/src/test/java/org/springframework/amqp/core/DeclarablesTests.java diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/Declarables.java b/spring-amqp/src/main/java/org/springframework/amqp/core/Declarables.java index a6726d85d2..a5892ef946 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/Declarables.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/Declarables.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2021 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. @@ -30,6 +30,7 @@ * broker using a single bean declaration for the collection. * * @author Gary Russell + * @author Björn Michael * @since 2.1 */ public class Declarables { @@ -42,7 +43,7 @@ public Declarables(Declarable... declarables) { } } - public Declarables(Collection declarables) { + public Declarables(Collection declarables) { Assert.notNull(declarables, "declarables cannot be null"); this.declarables.addAll(declarables); } @@ -58,11 +59,10 @@ public Collection getDeclarables() { * @return the filtered list. * @since 2.2 */ - @SuppressWarnings("unchecked") public List getDeclarablesByType(Class type) { return this.declarables.stream() .filter(type::isInstance) - .map(dec -> (T) dec) + .map(type::cast) .collect(Collectors.toList()); } diff --git a/spring-amqp/src/test/java/org/springframework/amqp/core/DeclarablesTests.java b/spring-amqp/src/test/java/org/springframework/amqp/core/DeclarablesTests.java new file mode 100644 index 0000000000..a1a9f12f61 --- /dev/null +++ b/spring-amqp/src/test/java/org/springframework/amqp/core/DeclarablesTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2021 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.core; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +/** + * @author Björn Michael + * @since 2.4 + */ +public class DeclarablesTests { + + @Test + public void getDeclarables() { + List queues = List.of( + new Queue("q1", false, false, true), + new Queue("q2", false, false, true)); + Declarables declarables = new Declarables(queues); + + assertThat(declarables.getDeclarables()).hasSameElementsAs(queues); + } + + @Test + public void getDeclarablesByType() { + Queue queue = new Queue("queue"); + TopicExchange exchange = new TopicExchange("exchange"); + Binding binding = BindingBuilder.bind(queue).to(exchange).with("foo.bar"); + Declarables declarables = new Declarables(queue, exchange, binding); + + assertThat(declarables.getDeclarablesByType(Queue.class)).containsExactlyInAnyOrder(queue); + assertThat(declarables.getDeclarablesByType(Exchange.class)).containsExactlyInAnyOrder(exchange); + assertThat(declarables.getDeclarablesByType(Binding.class)).containsExactlyInAnyOrder(binding); + assertThat(declarables.getDeclarablesByType(Declarable.class)).containsExactlyInAnyOrder( + queue, exchange, binding); + } + +} From 06396d64c845ae63469daba083ff16c819ea2adf Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 11 Nov 2021 10:02:53 -0500 Subject: [PATCH 037/737] Fix `overview.html` & add java 11 conf for tests --- build.gradle | 6 ++++++ src/api/overview.html | 19 +++++-------------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/build.gradle b/build.gradle index fe3877c7ff..1dd45b2406 100644 --- a/build.gradle +++ b/build.gradle @@ -144,6 +144,12 @@ subprojects { subproject -> targetCompatibility = 1.8 } + compileTestJava { + sourceCompatibility = 11 + targetCompatibility = 11 + options.encoding = 'UTF-8' + } + eclipse { project { natures += 'org.springframework.ide.eclipse.core.springnature' diff --git a/src/api/overview.html b/src/api/overview.html index f7208c81ab..a7ff43cd32 100644 --- a/src/api/overview.html +++ b/src/api/overview.html @@ -1,24 +1,15 @@ - This document is the API specification for Spring AMQP -
+ This document is the API specification for Spring AMQP +

- For further API reference and developer documentation, see the Spring AMQP reference documentation. That - documentation contains more detailed, developer-targeted - descriptions, with conceptual overviews, definitions of terms, - workarounds, and working code examples. + For further API reference and developer documentation, see the Spring AMQP reference documentation. + That documentation contains more detailed, developer-targeted descriptions, with conceptual overviews, definitions of terms, workarounds, and working code examples.

- If you are interested in commercial training, consultancy, and - support for Spring AMQP, please visit - https://www.spring.io + If you are interested in commercial training, consultancy, and support for Spring AMQP, please visit https://www.spring.io

From 3f003cfdfd0ef5245766e46c53bcd59651f6e305 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Sat, 11 Dec 2021 09:14:59 -0500 Subject: [PATCH 038/737] Upgrade Log4j to 2.15.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 1dd45b2406..1c7b74fca4 100644 --- a/build.gradle +++ b/build.gradle @@ -52,7 +52,7 @@ ext { jaywayJsonPathVersion = '2.4.0' junit4Version = '4.13.2' junitJupiterVersion = '5.7.2' - log4jVersion = '2.14.1' + log4jVersion = '2.15.0' logbackVersion = '1.2.3' lz4Version = '1.8.0' micrometerVersion = '1.8.0' From 07443ceb77272c165c12dc8154cac22da41dd10b Mon Sep 17 00:00:00 2001 From: zysaaa <982020642@qq.com> Date: Tue, 14 Dec 2021 02:24:41 +0800 Subject: [PATCH 039/737] GH-1401: SMLC: Fix setConcurrency Resolves https://github.com/spring-projects/spring-amqp/issues/1401 - multiple calls can temporarly inflate consumer count --- .../SimpleMessageListenerContainer.java | 1 - .../SimpleMessageListenerContainerTests.java | 28 ++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java index b77ec1c17e..338e9499d8 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java @@ -223,7 +223,6 @@ public void setConcurrency(String concurrency) { int maxConsumersToSet = Integer.parseInt(concurrency.substring(separatorIndex + 1)); Assert.isTrue(maxConsumersToSet >= consumersToSet, "'maxConcurrentConsumers' value must be at least 'concurrentConsumers'"); - this.concurrentConsumers = 1; this.maxConcurrentConsumers = null; setConcurrentConsumers(consumersToSet); setMaxConcurrentConsumers(maxConsumersToSet); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java index 32c133ddd0..ab8801216f 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -69,6 +69,7 @@ import org.springframework.amqp.core.AnonymousQueue; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageBuilder; +import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.core.MessagePostProcessor; import org.springframework.amqp.core.Queue; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; @@ -650,6 +651,31 @@ public Message postProcessMessage(Message message) throws AmqpException { assertThat(afterReceivePostProcessors).containsExactly(mpp2, mpp3); } + @Test + void setConcurrency() throws Exception { + ConnectionFactory connectionFactory = mock(ConnectionFactory.class); + Connection connection = mock(Connection.class); + Channel channel = mock(Channel.class); + given(connectionFactory.createConnection()).willReturn(connection); + given(connection.createChannel(false)).willReturn(channel); + final AtomicReference consumer = new AtomicReference<>(); + willAnswer(invocation -> { + consumer.set(invocation.getArgument(6)); + consumer.get().handleConsumeOk("1"); + return "1"; + }).given(channel) + .basicConsume(anyString(), anyBoolean(), anyString(), anyBoolean(), anyBoolean(), anyMap(), + any(Consumer.class)); + final SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); + container.setQueueNames("foo", "bar"); + container.setMessageListener(mock(MessageListener.class)); + container.setConcurrency("5-10"); + container.start(); + await().until(() -> TestUtils.getPropertyValue(container, "consumers", Collection.class).size() == 5); + container.setConcurrency("10-10"); + assertThat(TestUtils.getPropertyValue(container, "consumers", Collection.class)).hasSize(10); + } + private Answer messageToConsumer(final Channel mockChannel, final SimpleMessageListenerContainer container, final boolean cancel, final CountDownLatch latch) { return invocation -> { From a7622fb6b268d02622d0a8f23a3d1fd17d64ca6e Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 13 Dec 2021 17:23:01 -0500 Subject: [PATCH 040/737] Upgrade to Gradle 7.3.1 --- gradle.properties | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 59536 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 257 ++++++++++++++--------- 4 files changed, 156 insertions(+), 107 deletions(-) diff --git a/gradle.properties b/gradle.properties index 1415a98f92..c3976482eb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ version=2.4.1-SNAPSHOT org.gradlee.caching=true -org.gradle.jvmargs=-Xms512m -Xmx4g -XX:MaxPermSize=1024m -XX:MaxMetaspaceSize=1g +org.gradle.jvmargs=-Xms512m -Xmx4g org.gradle.daemon=true org.gradle.parallel=true kotlin.stdlib.default.dependency=false diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..7454180f2ae8848c63b8b4dea2cb829da983f2fa 100644 GIT binary patch delta 18435 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 B8dv}T delta 17845 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 X49Luk)baa85H9$5YCsTPT`SVRWMtMW diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9027973dc5..78e8b07ec0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionSha256Sum=8de6efc274ab52332a9c820366dd5cf5fc9d35ec7078fd70c8ec6913431ee610 +distributionSha256Sum=9afb3ca688fc12c761a0e9e4321e4d24e977a4a8916c8a768b1fe05ddb4d6b66 diff --git a/gradlew b/gradlew index 4f906e0c81..1b6c787337 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,67 +17,101 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" From 00854cdb1be3c6f7e22557017bf67b28d60f4f8b Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 14 Dec 2021 09:07:52 -0500 Subject: [PATCH 041/737] Fix duplicatesStrategy for docs Gradle task --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 1c7b74fca4..37aab2a626 100644 --- a/build.gradle +++ b/build.gradle @@ -478,6 +478,7 @@ task prepareAsciidocBuild(type: Sync) { from { configurations.docs.collect { zipTree(it) } } + duplicatesStrategy = DuplicatesStrategy.EXCLUDE from 'src/reference/asciidoc/' into "$buildDir/asciidoc" } From 8b18e900604390d643010085927e563eff919f16 Mon Sep 17 00:00:00 2001 From: zysaaa <982020642@qq.com> Date: Tue, 14 Dec 2021 10:44:52 +0800 Subject: [PATCH 042/737] GH-1402: SMLC: Fix BlockingQueueConsumer#queue init --- .../amqp/rabbit/listener/BlockingQueueConsumer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java index 804c1a8887..defca73faf 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java @@ -295,7 +295,7 @@ public BlockingQueueConsumer(ConnectionFactory connectionFactory, this.noLocal = noLocal; this.exclusive = exclusive; this.queues = Arrays.copyOf(queues, queues.length); - this.queue = new LinkedBlockingQueue(prefetchCount); + this.queue = new LinkedBlockingQueue(queues.length == 0 ? prefetchCount : prefetchCount * queues.length); } public Channel getChannel() { From cd429dcf9e068ca2c4728d65dc96e6163b9d6e0b Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 14 Dec 2021 17:25:15 -0500 Subject: [PATCH 043/737] Upgrade Log4j to 2.16.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 37aab2a626..edf666ba34 100644 --- a/build.gradle +++ b/build.gradle @@ -52,7 +52,7 @@ ext { jaywayJsonPathVersion = '2.4.0' junit4Version = '4.13.2' junitJupiterVersion = '5.7.2' - log4jVersion = '2.15.0' + log4jVersion = '2.16.0' logbackVersion = '1.2.3' lz4Version = '1.8.0' micrometerVersion = '1.8.0' From 270f62d2df7e185e293b0210c96da9dbce33a6af Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 15 Dec 2021 15:22:20 -0500 Subject: [PATCH 044/737] GH-1406: Fix Possible Double Ack in Consumer Batch Resolves https://github.com/spring-projects/spring-amqp/issues/1406 Previously, if an MPP caused the last record in an ack to be skipped two acks for the same deliveryTag would be sent causine a channel shutdown. There is no need to ack skipped records because the entire batch is acked (with basicAck multiple=true). **cherry-pick to 2.3.x, 2.2.x** --- .../SimpleMessageListenerContainer.java | 1 - .../SimpleMessageListenerContainerTests.java | 48 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java index 338e9499d8..f1a7b47003 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java @@ -937,7 +937,6 @@ private boolean doReceiveAndExecute(BlockingQueueConsumer consumer) throws Excep for (MessagePostProcessor processor : getAfterReceivePostProcessors()) { message = processor.postProcessMessage(message); if (message == null) { - channel.basicAck(deliveryTag, false); if (this.logger.isDebugEnabled()) { this.logger.debug( "Message Post Processor returned 'null', discarding message " + original); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java index ab8801216f..35fbf41381 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java @@ -26,15 +26,18 @@ import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willAnswer; import static org.mockito.BDDMockito.willReturn; import static org.mockito.BDDMockito.willThrow; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import java.io.IOException; import java.net.URL; @@ -67,6 +70,7 @@ import org.springframework.amqp.ImmediateAcknowledgeAmqpException; import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.AnonymousQueue; +import org.springframework.amqp.core.BatchMessageListener; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageBuilder; import org.springframework.amqp.core.MessageListener; @@ -651,6 +655,7 @@ public Message postProcessMessage(Message message) throws AmqpException { assertThat(afterReceivePostProcessors).containsExactly(mpp2, mpp3); } + @SuppressWarnings("unchecked") @Test void setConcurrency() throws Exception { ConnectionFactory connectionFactory = mock(ConnectionFactory.class); @@ -676,6 +681,49 @@ void setConcurrency() throws Exception { assertThat(TestUtils.getPropertyValue(container, "consumers", Collection.class)).hasSize(10); } + @Test + void filterMppNoDoubleAck() throws Exception { + ConnectionFactory connectionFactory = mock(ConnectionFactory.class); + Connection connection = mock(Connection.class); + Channel channel = mock(Channel.class); + given(connectionFactory.createConnection()).willReturn(connection); + given(connection.createChannel(false)).willReturn(channel); + final AtomicReference consumer = new AtomicReference<>(); + willAnswer(invocation -> { + consumer.set(invocation.getArgument(6)); + consumer.get().handleConsumeOk("1"); + return "1"; + }).given(channel) + .basicConsume(anyString(), anyBoolean(), anyString(), anyBoolean(), anyBoolean(), anyMap(), + any(Consumer.class)); + final CountDownLatch latch = new CountDownLatch(1); + willAnswer(invocation -> { + latch.countDown(); + return null; + }).given(channel).basicAck(anyLong(), anyBoolean()); + + final SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); + container.setAfterReceivePostProcessors(msg -> null); + container.setQueueNames("foo"); + MessageListener listener = mock(BatchMessageListener.class); + container.setMessageListener(listener); + container.setBatchSize(2); + container.setConsumerBatchEnabled(true); + container.start(); + BasicProperties props = new BasicProperties(); + byte[] payload = "baz".getBytes(); + Envelope envelope = new Envelope(1L, false, "foo", "bar"); + consumer.get().handleDelivery("1", envelope, props, payload); + envelope = new Envelope(2L, false, "foo", "bar"); + consumer.get().handleDelivery("1", envelope, props, payload); + assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); + verify(channel, never()).basicAck(eq(1), anyBoolean()); + verify(channel).basicAck(2, true); + container.stop(); + verify(listener).containerAckMode(AcknowledgeMode.AUTO); + verifyNoMoreInteractions(listener); + } + private Answer messageToConsumer(final Channel mockChannel, final SimpleMessageListenerContainer container, final boolean cancel, final CountDownLatch latch) { return invocation -> { From 83e57bb73132b121a22870901a86939ef17b7b5d Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 15 Dec 2021 16:21:07 -0500 Subject: [PATCH 045/737] Improve Mock Test Runtime Set shutdown timeout to 0, especially when changing queues. --- .../listener/SimpleMessageListenerContainerTests.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java index 35fbf41381..d040c39a37 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java @@ -159,6 +159,7 @@ public void testDefaultConsumerCount() { container.setMessageListener(new MessageListenerAdapter(this)); container.setQueueNames("foo"); container.setAutoStartup(false); + container.setShutdownTimeout(0); container.afterPropertiesSet(); assertThat(ReflectionTestUtils.getField(container, "concurrentConsumers")).isEqualTo(1); container.stop(); @@ -262,6 +263,8 @@ public void testTxSizeAcksWIthShortSet() throws Exception { container.setQueueNames("foobar"); container.setBatchSize(2); container.setMessageListener(messages::add); + container.setShutdownTimeout(0); + container.afterPropertiesSet(); container.start(); BasicProperties props = new BasicProperties(); byte[] payload = "baz".getBytes(); @@ -308,6 +311,7 @@ public void testConsumerArgs() throws Exception { container.setMessageListener(message -> { }); container.setConsumerArguments(Collections.singletonMap("x-priority", 10)); + container.setShutdownTimeout(0); container.afterPropertiesSet(); container.start(); verify(channel).basicConsume(anyString(), anyBoolean(), anyString(), anyBoolean(), anyBoolean(), @@ -343,6 +347,7 @@ public void testChangeQueues() throws Exception { container.setReceiveTimeout(1); container.setMessageListener(message -> { }); + container.setShutdownTimeout(0); container.afterPropertiesSet(); container.start(); assertThat(latch1.await(10, TimeUnit.SECONDS)).isTrue(); @@ -389,6 +394,7 @@ public void testAddQueuesAndStartInCycle() throws Exception { final SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); container.setMessageListener(message -> { }); + container.setShutdownTimeout(0); container.afterPropertiesSet(); for (int i = 0; i < 10; i++) { From 687b515a07f571470a072c656c41950ff92cff75 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Sun, 19 Dec 2021 15:50:37 -0500 Subject: [PATCH 046/737] Upgrade Log4j to 2.17.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index edf666ba34..f15d4ca6af 100644 --- a/build.gradle +++ b/build.gradle @@ -52,7 +52,7 @@ ext { jaywayJsonPathVersion = '2.4.0' junit4Version = '4.13.2' junitJupiterVersion = '5.7.2' - log4jVersion = '2.16.0' + log4jVersion = '2.17.0' logbackVersion = '1.2.3' lz4Version = '1.8.0' micrometerVersion = '1.8.0' From a4f014dc359bde4b8d6224df4996f0e395c2efb5 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 20 Dec 2021 11:44:08 -0500 Subject: [PATCH 047/737] GH-1409: Fix Nacks for Async Replies Resolves https://github.com/spring-projects/spring-amqp/issues/1409 Normally, when message has a fatal exception (such as message conversion) `basicNack` with `multiple` true is used, to nack any previously unacked messages (e.g. when using batch size to limit the ack traffic). Even when using manual acks, fatal exceptions are nacked by the container because the user does not have access to the message. However, when using async replies, this has the side effect of nacking unprocessed messages. Detect whether async replies are being used and only nack individual records that cause fatal exceptions. Also, coerce the `AcknowledgeMode` to `MANUAL` for such listners. Add a test for both containers; send a good message followed by a bad one without actually completing the reply future. After the exception occurs and the container is stopped, there should be one messag in the queue. * Remove warning, deprecation; add docs. * Docs. **Cherry-pick to `2.3.x` & `2.2.x`** --- .../amqp/core/MessageListener.java | 12 +- .../AbstractMessageListenerContainer.java | 10 +- .../listener/BlockingQueueConsumer.java | 38 +++- .../DirectMessageListenerContainer.java | 2 +- .../SimpleMessageListenerContainer.java | 7 +- .../AbstractAdaptableMessageListener.java | 19 +- .../adapter/DelegatingInvocableHandler.java | 25 ++- .../listener/adapter/HandlerAdapter.java | 16 ++ .../MessagingMessageListenerAdapter.java | 5 + .../rabbit/listener/adapter/MonoHandler.java | 44 +++++ .../rabbit/listener/AsyncReplyToTests.java | 178 ++++++++++++++++++ .../SimpleMessageListenerContainerTests.java | 1 + src/reference/asciidoc/amqp.adoc | 3 + 13 files changed, 328 insertions(+), 32 deletions(-) create mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MonoHandler.java create mode 100644 spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/AsyncReplyToTests.java diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageListener.java b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageListener.java index e0dbe8397f..34f786dd44 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageListener.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 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. @@ -43,6 +43,16 @@ default void containerAckMode(AcknowledgeMode mode) { // NOSONAR - empty } + /** + * Return true if this listener is request/reply and the replies are + * async. + * @return true for async replies. + * @since 2.2.21 + */ + default boolean isAsyncReplies() { + return false; + } + /** * Delivers a batch of messages. * @param messages the messages. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index 7a89f2df85..374f227902 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -255,6 +255,8 @@ public abstract class AbstractMessageListenerContainer extends RabbitAccessor private volatile boolean lazyLoad; + private boolean asyncReplies; + @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; @@ -439,6 +441,7 @@ public void setMessageListener(MessageListener messageListener) { this.messageListener = messageListener; this.isBatchListener = messageListener instanceof BatchMessageListener || messageListener instanceof ChannelAwareBatchMessageListener; + this.asyncReplies = messageListener.isAsyncReplies(); } /** @@ -1016,10 +1019,12 @@ public boolean isPossibleAuthenticationFailureFatal() { return this.possibleAuthenticationFailureFatal; } - protected boolean isPossibleAuthenticationFailureFatalSet() { return this.possibleAuthenticationFailureFatalSet; } + protected boolean isAsyncReplies() { + return this.asyncReplies; + } /** * Set to true to automatically declare elements (queues, exchanges, bindings) @@ -1220,6 +1225,9 @@ public void afterPropertiesSet() { catch (IllegalStateException e) { this.logger.debug("Could not enable micrometer timers", e); } + if (this.isAsyncReplies() && !AcknowledgeMode.MANUAL.equals(this.acknowledgeMode)) { + this.acknowledgeMode = AcknowledgeMode.MANUAL; + } } @Override diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java index defca73faf..263ac54604 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java @@ -779,6 +779,17 @@ public synchronized void stop() { * @param ex the thrown application exception or error */ public void rollbackOnExceptionIfNecessary(Throwable ex) { + rollbackOnExceptionIfNecessary(ex, -1); + } + + /** + * Perform a rollback, handling rollback exceptions properly. + * @param ex the thrown application exception or error + * @param tag delivery tag; when specified (greater than or equal to 0) only that + * message is nacked. + * @since 2.2.21. + */ + public void rollbackOnExceptionIfNecessary(Throwable ex, long tag) { boolean ackRequired = !this.acknowledgeMode.isAutoAck() && (!this.acknowledgeMode.isManual() || ContainerUtils.isRejectManual(ex)); @@ -790,14 +801,20 @@ public void rollbackOnExceptionIfNecessary(Throwable ex) { RabbitUtils.rollbackIfNecessary(this.channel); } if (ackRequired) { - OptionalLong deliveryTag = this.deliveryTags.stream().mapToLong(l -> l).max(); - if (deliveryTag.isPresent()) { - this.channel.basicNack(deliveryTag.getAsLong(), true, - ContainerUtils.shouldRequeue(this.defaultRequeueRejected, ex, logger)); + if (tag < 0) { + OptionalLong deliveryTag = this.deliveryTags.stream().mapToLong(l -> l).max(); + if (deliveryTag.isPresent()) { + this.channel.basicNack(deliveryTag.getAsLong(), true, + ContainerUtils.shouldRequeue(this.defaultRequeueRejected, ex, logger)); + } + if (this.transactional) { + // Need to commit the reject (=nack) + RabbitUtils.commitIfNecessary(this.channel); + } } - if (this.transactional) { - // Need to commit the reject (=nack) - RabbitUtils.commitIfNecessary(this.channel); + else { + this.channel.basicNack(tag, false, + ContainerUtils.shouldRequeue(this.defaultRequeueRejected, ex, logger)); } } } @@ -806,7 +823,12 @@ public void rollbackOnExceptionIfNecessary(Throwable ex) { throw RabbitExceptionTranslator.convertRabbitAccessException(e); // NOSONAR stack trace loss } finally { - this.deliveryTags.clear(); + if (tag < 0) { + this.deliveryTags.clear(); + } + else { + this.deliveryTags.remove(tag); + } } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java index 3a816caa44..5b92c81ccc 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java @@ -1214,7 +1214,7 @@ private void rollback(long deliveryTag, Exception e) { } } } - getChannel().basicNack(deliveryTag, true, + getChannel().basicNack(deliveryTag, !isAsyncReplies(), ContainerUtils.shouldRequeue(isDefaultRequeueRejected(), e, this.logger)); } catch (IOException e1) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java index f1a7b47003..f0cd3ee91f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java @@ -982,6 +982,9 @@ private boolean doReceiveAndExecute(BlockingQueueConsumer consumer) throws Excep } break; } + long tagToRollback = isAsyncReplies() + ? message.getMessageProperties().getDeliveryTag() + : -1; if (getTransactionManager() != null) { if (getTransactionAttribute().rollbackOn(ex)) { RabbitResourceHolder resourceHolder = (RabbitResourceHolder) TransactionSynchronizationManager @@ -994,7 +997,7 @@ private boolean doReceiveAndExecute(BlockingQueueConsumer consumer) throws Excep * If we don't actually have a transaction, we have to roll back * manually. See prepareHolderForRollback(). */ - consumer.rollbackOnExceptionIfNecessary(ex); + consumer.rollbackOnExceptionIfNecessary(ex, tagToRollback); } throw ex; // encompassing transaction will handle the rollback. } @@ -1006,7 +1009,7 @@ private boolean doReceiveAndExecute(BlockingQueueConsumer consumer) throws Excep } } else { - consumer.rollbackOnExceptionIfNecessary(ex); + consumer.rollbackOnExceptionIfNecessary(ex, tagToRollback); throw ex; } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java index 03b4e84159..8746f17767 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java @@ -21,7 +21,6 @@ import java.lang.reflect.Type; import java.lang.reflect.WildcardType; import java.util.Arrays; -import java.util.function.Consumer; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -56,7 +55,6 @@ import org.springframework.util.concurrent.ListenableFuture; import com.rabbitmq.client.Channel; -import reactor.core.publisher.Mono; /** * An abstract {@link org.springframework.amqp.core.MessageListener} adapter providing the @@ -81,7 +79,7 @@ public abstract class AbstractAdaptableMessageListener implements ChannelAwareMe private static final ParserContext PARSER_CONTEXT = new TemplateParserContext("!{", "}"); - private static final boolean monoPresent = // NOSONAR - lower case + static final boolean monoPresent = // NOSONAR - lower case, protected ClassUtils.isPresent("reactor.core.publisher.Mono", ChannelAwareMessageListener.class.getClassLoader()); /** @@ -695,19 +693,4 @@ public Object getResult() { } - private static class MonoHandler { // NOSONAR - pointless to name it ..Utils|Helper - - static boolean isMono(Object result) { - return result instanceof Mono; - } - - @SuppressWarnings("unchecked") - static void subscribe(Object returnValue, Consumer success, - Consumer failure, Runnable completeConsumer) { - - ((Mono) returnValue).subscribe(success, failure, completeConsumer); - } - - } - } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandler.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandler.java index 8d21b14b29..96bb30fc36 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandler.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandler.java @@ -20,6 +20,7 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -44,6 +45,7 @@ import org.springframework.messaging.handler.annotation.support.PayloadMethodArgumentResolver; import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; import org.springframework.util.Assert; +import org.springframework.util.concurrent.ListenableFuture; import org.springframework.validation.Validator; @@ -84,6 +86,8 @@ public class DelegatingInvocableHandler { private final PayloadValidator validator; + private final boolean asyncReplies; + /** * Construct an instance with the supplied handlers for the bean. * @param handlers the handlers. @@ -132,9 +136,19 @@ public DelegatingInvocableHandler(List handlers, this.resolver = beanExpressionResolver; this.beanExpressionContext = beanExpressionContext; this.validator = validator == null ? null : new PayloadValidator(validator); + boolean asyncReplies; + asyncReplies = defaultHandler != null && isAsyncReply(defaultHandler); + Iterator iterator = handlers.iterator(); + while (iterator.hasNext()) { + asyncReplies |= isAsyncReply(iterator.next()); + } + this.asyncReplies = asyncReplies; } - + private boolean isAsyncReply(InvocableHandlerMethod method) { + return (AbstractAdaptableMessageListener.monoPresent && MonoHandler.isMono(method.getMethod().getReturnType())) + || ListenableFuture.class.isAssignableFrom(method.getMethod().getReturnType()); + } /** * @return the bean @@ -143,6 +157,15 @@ public Object getBean() { return this.bean; } + /** + * Return true if any handler method has an async reply type. + * @return the asyncReply. + * @since 2.2.21 + */ + public boolean isAsyncReplies() { + return this.asyncReplies; + } + /** * Invoke the method with the given message. * @param message the message. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/HandlerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/HandlerAdapter.java index fbc010690f..29d3bd45fd 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/HandlerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/HandlerAdapter.java @@ -22,6 +22,7 @@ import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; +import org.springframework.util.concurrent.ListenableFuture; /** * A wrapper for either an {@link InvocableHandlerMethod} or @@ -38,6 +39,8 @@ public class HandlerAdapter { private final DelegatingInvocableHandler delegatingHandler; + private final boolean asyncReplies; + /** * Construct an instance with the provided method. * @param invokerHandlerMethod the method. @@ -45,6 +48,9 @@ public class HandlerAdapter { public HandlerAdapter(InvocableHandlerMethod invokerHandlerMethod) { this.invokerHandlerMethod = invokerHandlerMethod; this.delegatingHandler = null; + this.asyncReplies = (AbstractAdaptableMessageListener.monoPresent + && MonoHandler.isMono(invokerHandlerMethod.getMethod().getReturnType())) + || ListenableFuture.class.isAssignableFrom(invokerHandlerMethod.getMethod().getReturnType()); } /** @@ -54,6 +60,7 @@ public HandlerAdapter(InvocableHandlerMethod invokerHandlerMethod) { public HandlerAdapter(DelegatingInvocableHandler delegatingHandler) { this.invokerHandlerMethod = null; this.delegatingHandler = delegatingHandler; + this.asyncReplies = delegatingHandler.isAsyncReplies(); } /** @@ -139,6 +146,15 @@ public Object getBean() { } } + /** + * Return true if any handler method has an async reply type. + * @return the asyncReply. + * @since 2.2.21 + */ + public boolean isAsyncReplies() { + return this.asyncReplies; + } + /** * Build an {@link InvocationResult} for the result and inbound payload. * @param result the result. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java index ce4b30a416..fc585a1b1b 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java @@ -107,6 +107,11 @@ protected HandlerAdapter getHandlerAdapter() { return this.handlerAdapter; } + @Override + public boolean isAsyncReplies() { + return this.handlerAdapter.isAsyncReplies(); + } + /** * Set the {@link AmqpHeaderMapper} implementation to use to map the standard * AMQP headers. By default, a {@link org.springframework.amqp.support.SimpleAmqpHeaderMapper diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MonoHandler.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MonoHandler.java new file mode 100644 index 0000000000..f594e44eab --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MonoHandler.java @@ -0,0 +1,44 @@ +/* + * Copyright 2021 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 java.util.function.Consumer; + +import reactor.core.publisher.Mono; + +/** + * Class to prevent direct links to {@link Mono}. + * @author Gary Russell + * @since 2.2.21 + */ +final class MonoHandler { // NOSONAR - pointless to name it ..Utils|Helper + + private MonoHandler() { + } + + static boolean isMono(Object result) { + return result instanceof Mono; + } + + @SuppressWarnings("unchecked") + static void subscribe(Object returnValue, Consumer success, + Consumer failure, Runnable completeConsumer) { + + ((Mono) returnValue).subscribe(success, failure, completeConsumer); + } + +} diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/AsyncReplyToTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/AsyncReplyToTests.java new file mode 100644 index 0000000000..3bfab60541 --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/AsyncReplyToTests.java @@ -0,0 +1,178 @@ +/* + * Copyright 2021 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; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.core.AcknowledgeMode; +import org.springframework.amqp.core.MessageBuilder; +import org.springframework.amqp.core.MessagePropertiesBuilder; +import org.springframework.amqp.rabbit.annotation.EnableRabbit; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.config.DirectRabbitListenerContainerFactory; +import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.junit.RabbitAvailable; +import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.concurrent.SettableListenableFuture; + +import com.rabbitmq.client.Channel; + +/** + * @author Gary Russell + * @since 2.2.21 + * + */ +@SpringJUnitConfig +@RabbitAvailable(queues = { "async1", "async2" }) +public class AsyncReplyToTests { + + @Test + void ackSingleWhenFatalSMLC(@Autowired Config config, @Autowired RabbitListenerEndpointRegistry registry, + @Autowired RabbitTemplate template, @Autowired RabbitAdmin admin) throws IOException, InterruptedException { + + template.send("async1", MessageBuilder.withBody("\"foo\"".getBytes()).andProperties( + MessagePropertiesBuilder.newInstance() + .setContentType("application/json") + .setReplyTo("nowhere") + .build()) + .build()); + template.send("async1", MessageBuilder.withBody("junk".getBytes()).andProperties( + MessagePropertiesBuilder.newInstance() + .setContentType("application/json") + .setReplyTo("nowhere") + .build()) + .build()); + assertThat(config.smlcLatch.await(10, TimeUnit.SECONDS)).isTrue(); + registry.getListenerContainer("smlc").stop(); + assertThat(admin.getQueueInfo("async1").getMessageCount()).isEqualTo(1); + } + + @Test + void ackSingleWhenFatalDMLC(@Autowired Config config, @Autowired RabbitListenerEndpointRegistry registry, + @Autowired RabbitTemplate template, @Autowired RabbitAdmin admin) throws IOException, InterruptedException { + + template.send("async2", MessageBuilder.withBody("\"foo\"".getBytes()).andProperties( + MessagePropertiesBuilder.newInstance() + .setContentType("application/json") + .setReplyTo("nowhere") + .build()) + .build()); + template.send("async2", MessageBuilder.withBody("junk".getBytes()).andProperties( + MessagePropertiesBuilder.newInstance() + .setContentType("application/json") + .setReplyTo("nowhere") + .build()) + .build()); + assertThat(config.dmlcLatch.await(10, TimeUnit.SECONDS)).isTrue(); + registry.getListenerContainer("dmlc").stop(); + assertThat(admin.getQueueInfo("async2").getMessageCount()).isEqualTo(1); + } + + @Configuration + @EnableRabbit + static class Config { + + volatile CountDownLatch smlcLatch = new CountDownLatch(1); + + volatile CountDownLatch dmlcLatch = new CountDownLatch(1); + + @RabbitListener(id = "smlc", queues = "async1", containerFactory = "smlcf") + ListenableFuture listen1(String in, Channel channel) { + return new SettableListenableFuture<>(); + } + + @RabbitListener(id = "dmlc", queues = "async2", containerFactory = "dmlcf") + ListenableFuture listen2(String in, Channel channel) { + return new SettableListenableFuture<>(); + } + + @Bean + MessageConverter converter() { + return new Jackson2JsonMessageConverter(); + } + + @Bean + ConnectionFactory cf() throws IOException, TimeoutException { + return new CachingConnectionFactory(RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); + } + + @Bean + SimpleRabbitListenerContainerFactory smlcf(ConnectionFactory cf, MessageConverter converter) { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(cf); + factory.setAcknowledgeMode(AcknowledgeMode.MANUAL); + factory.setMessageConverter(converter); + factory.setErrorHandler(new ConditionalRejectingErrorHandler() { + + @Override + public void handleError(Throwable t) { + smlcLatch.countDown(); + super.handleError(t); + } + + }); + return factory; + } + + @Bean + DirectRabbitListenerContainerFactory dmlcf(ConnectionFactory cf, MessageConverter converter) { + DirectRabbitListenerContainerFactory factory = new DirectRabbitListenerContainerFactory(); + factory.setConnectionFactory(cf); + factory.setAcknowledgeMode(AcknowledgeMode.MANUAL); + factory.setMessageConverter(converter); + factory.setErrorHandler(new ConditionalRejectingErrorHandler() { + + @Override + public void handleError(Throwable t) { + dmlcLatch.countDown(); + super.handleError(t); + } + + }); + return factory; + } + + @Bean + RabbitTemplate template(ConnectionFactory cf) { + return new RabbitTemplate(cf); + } + + @Bean + RabbitAdmin admin(ConnectionFactory cf) { + return new RabbitAdmin(cf); + } + + } +} diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java index d040c39a37..c5f09aaadc 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java @@ -727,6 +727,7 @@ void filterMppNoDoubleAck() throws Exception { verify(channel).basicAck(2, true); container.stop(); verify(listener).containerAckMode(AcknowledgeMode.AUTO); + verify(listener).isAsyncReplies(); verifyNoMoreInteractions(listener); } diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index a471d8e36e..d76a45cd4d 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -3535,6 +3535,9 @@ If the async result is completed with an `AmqpRejectAndDontRequeueException`, th 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]] ===== Threading and Asynchronous Consumers From 00745a8cd1e203667a40842eb7953ad05409dabf Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 20 Dec 2021 11:51:33 -0500 Subject: [PATCH 048/737] Upgrade versions; prepare for release --- build.gradle | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index f15d4ca6af..36445f903d 100644 --- a/build.gradle +++ b/build.gradle @@ -39,19 +39,19 @@ ext { modifiedFiles = files(grgit.status().unstaged.modified).filter{ f -> f.name.endsWith('.java') || f.name.endsWith('.kt') } - assertjVersion = '3.20.2' + assertjVersion = '3.21.0' assertkVersion = '0.24' - awaitilityVersion = '4.1.0' + awaitilityVersion = '4.1.1' commonsCompressVersion = '1.20' commonsHttpClientVersion = '4.5.13' commonsPoolVersion = '2.11.1' googleJsr305Version = '3.0.2' hamcrestVersion = '2.2' hibernateValidationVersion = '6.2.0.Final' - jacksonBomVersion = '2.13.0' + jacksonBomVersion = '2.13.1' jaywayJsonPathVersion = '2.4.0' junit4Version = '4.13.2' - junitJupiterVersion = '5.7.2' + junitJupiterVersion = '5.8.2' log4jVersion = '2.17.0' logbackVersion = '1.2.3' lz4Version = '1.8.0' @@ -60,10 +60,10 @@ ext { rabbitmqStreamVersion = '0.4.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.13.1' rabbitmqHttpClientVersion = '3.12.1' - reactorVersion = '2020.0.13' + reactorVersion = '2020.0.14' snappyVersion = '1.1.8.4' springDataCommonsVersion = '2.6.0' - springVersion = project.hasProperty('springVersion') ? project.springVersion : '5.3.13' + springVersion = project.hasProperty('springVersion') ? project.springVersion : '5.3.14' springRetryVersion = '1.3.1' zstdJniVersion = '1.5.0-2' } From 9a2a5a0c7b8edf102377a004c119d5d664ec12a9 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 20 Dec 2021 17:54:30 +0000 Subject: [PATCH 049/737] [artifactory-release] Release version 2.4.1 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index c3976482eb..d403eade49 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=2.4.1-SNAPSHOT +version=2.4.1 org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g org.gradle.daemon=true From 6189b1005c677566b9460aaf9046eab45553c3ba Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 20 Dec 2021 17:54:32 +0000 Subject: [PATCH 050/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index d403eade49..d9a850bc6e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=2.4.1 +version=2.4.2-SNAPSHOT org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g org.gradle.daemon=true From 525172bb07b92b94584bf39c97c6b017b6de96b8 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 21 Dec 2021 11:49:34 -0500 Subject: [PATCH 051/737] Fix Sonar Issue --- .../BaseRabbitListenerContainerFactory.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BaseRabbitListenerContainerFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BaseRabbitListenerContainerFactory.java index 1dd5804d80..4c4f042c33 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BaseRabbitListenerContainerFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BaseRabbitListenerContainerFactory.java @@ -110,18 +110,21 @@ protected void applyCommonOverrides(@Nullable RabbitListenerEndpoint endpoint, C instance.setListenerId(endpoint.getId()); endpoint.setupListenerContainer(instance); } - if (instance.getMessageListener() instanceof AbstractAdaptableMessageListener) { - AbstractAdaptableMessageListener messageListener = (AbstractAdaptableMessageListener) instance - .getMessageListener(); + Object iml = instance.getMessageListener(); + if (iml instanceof AbstractAdaptableMessageListener) { + AbstractAdaptableMessageListener messageListener = (AbstractAdaptableMessageListener) iml; JavaUtils.INSTANCE // NOSONAR .acceptIfNotNull(this.beforeSendReplyPostProcessors, messageListener::setBeforeSendReplyPostProcessors) .acceptIfNotNull(this.retryTemplate, messageListener::setRetryTemplate) .acceptIfCondition(this.retryTemplate != null && this.recoveryCallback != null, this.recoveryCallback, messageListener::setRecoveryCallback) - .acceptIfNotNull(this.defaultRequeueRejected, messageListener::setDefaultRequeueRejected) - .acceptIfNotNull(endpoint.getReplyPostProcessor(), messageListener::setReplyPostProcessor) - .acceptIfNotNull(endpoint.getReplyContentType(), messageListener::setReplyContentType); + .acceptIfNotNull(this.defaultRequeueRejected, messageListener::setDefaultRequeueRejected); + if (endpoint != null) { + JavaUtils.INSTANCE + .acceptIfNotNull(endpoint.getReplyPostProcessor(), messageListener::setReplyPostProcessor) + .acceptIfNotNull(endpoint.getReplyContentType(), messageListener::setReplyContentType); + } messageListener.setConverterWinsContentType(endpoint.isConverterWinsContentType()); } } From d3b93e963e8ed784b6fb28a29b187af35d470405 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 21 Dec 2021 12:11:43 -0500 Subject: [PATCH 052/737] Fix Sonar Issue --- .../amqp/rabbit/config/BaseRabbitListenerContainerFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BaseRabbitListenerContainerFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BaseRabbitListenerContainerFactory.java index 4c4f042c33..426d1fa62a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BaseRabbitListenerContainerFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BaseRabbitListenerContainerFactory.java @@ -124,8 +124,8 @@ protected void applyCommonOverrides(@Nullable RabbitListenerEndpoint endpoint, C JavaUtils.INSTANCE .acceptIfNotNull(endpoint.getReplyPostProcessor(), messageListener::setReplyPostProcessor) .acceptIfNotNull(endpoint.getReplyContentType(), messageListener::setReplyContentType); + messageListener.setConverterWinsContentType(endpoint.isConverterWinsContentType()); } - messageListener.setConverterWinsContentType(endpoint.isConverterWinsContentType()); } } From cf96793950ddc9f4967e7762af8ba36ca565e109 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 4 Jan 2022 09:20:10 -0500 Subject: [PATCH 053/737] GH-1412: Fix Messaging Template * GH-1412: Fix Messaging Template Resolves https://github.com/spring-projects/spring-amqp/issues/1412 The use of the `defaultDestination` is not correct when the template is used for both sends and receives because it is a queue name for receives and a routing key for sends. Add a new option to use the template's configured default receive queue for receive only methods. False by default to avoid a breaking change; it should be true by default in a future release. * Fix Javadocs **cherry-pick to 2.3.x, 2.2.x** --- .../rabbit/core/RabbitMessagingTemplate.java | 39 ++++++++++++++++++- .../amqp/rabbit/core/RabbitTemplate.java | 12 +++++- .../core/RabbitMessagingTemplateTests.java | 16 +++++++- 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplate.java index e323cd5bda..0f64eac1a2 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -48,6 +48,8 @@ public class RabbitMessagingTemplate extends AbstractMessagingTemplate private boolean converterSet; + private boolean useTemplateDefaultReceiveQueue; + /** * Constructor for use with bean properties. @@ -71,6 +73,7 @@ public RabbitMessagingTemplate(RabbitTemplate rabbitTemplate) { * @param rabbitTemplate the template. */ public void setRabbitTemplate(RabbitTemplate rabbitTemplate) { + Assert.notNull(rabbitTemplate, "'rabbitTemplate' must not be null"); this.rabbitTemplate = rabbitTemplate; } @@ -107,6 +110,18 @@ public MessageConverter getAmqpMessageConverter() { return this.amqpMessageConverter; } + /** + * When true, use the underlying {@link RabbitTemplate}'s defaultReceiveQueue property + * (if configured) for receive only methods instead of the {@code defaultDestination} + * configured in this template. Set this to true to use the template's queue instead. + * Default false, but will be true in a future release. + * @param useTemplateDefaultReceiveQueue true to use the template's queue. + * @since 2.2.22 + */ + public void setUseTemplateDefaultReceiveQueue(boolean useTemplateDefaultReceiveQueue) { + this.useTemplateDefaultReceiveQueue = useTemplateDefaultReceiveQueue; + } + @Override public void afterPropertiesSet() { Assert.notNull(getRabbitTemplate(), "Property 'rabbitTemplate' is required"); @@ -225,6 +240,28 @@ protected void doSend(String exchange, String routingKey, Message message) { } } + @Override + @Nullable + public Message receive() { + return doReceive(resolveDestination()); + } + + @Override + @Nullable + public T receiveAndConvert(Class targetClass) { + return receiveAndConvert(resolveDestination(), targetClass); + } + + private String resolveDestination() { + String dest = null; + if (this.useTemplateDefaultReceiveQueue) { + dest = this.rabbitTemplate.getDefaultReceiveQueue(); + } + if (dest == null) { + dest = getRequiredDefaultDestination(); + } + return dest; + } @Override protected Message doReceive(String destination) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java index 652c0c634a..ac4bd4b6f9 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -348,6 +348,16 @@ public void setDefaultReceiveQueue(String queue) { this.defaultReceiveQueue = queue; } + /** + * Return the configured default receive queue. + * @return the queue or null if not configured. + * @since 2.2.22 + */ + @Nullable + public String getDefaultReceiveQueue() { + return this.defaultReceiveQueue; + } + /** * The encoding to use when converting between byte arrays and Strings in message properties. * diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplateTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplateTests.java index f1db80c69d..d42b134472 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplateTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -256,6 +256,20 @@ public void receiveDefaultDestination() { assertTextMessage(message); } + @Test + public void receiveDefaultDestinationOverride() { + messagingTemplate.setDefaultDestination("defaultDest"); + messagingTemplate.setUseTemplateDefaultReceiveQueue(true); + + org.springframework.amqp.core.Message amqpMsg = createAmqpTextMessage(); + given(rabbitTemplate.getDefaultReceiveQueue()).willReturn("default"); + given(rabbitTemplate.receive("default")).willReturn(amqpMsg); + + Message message = messagingTemplate.receive(); + verify(rabbitTemplate).receive("default"); + assertTextMessage(message); + } + @Test public void receiveNoDefaultSet() { assertThatIllegalStateException() From 3fed4449cfe848a6d4f20403491f8eb61db80ac4 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 3 Jan 2022 16:30:59 -0500 Subject: [PATCH 054/737] GH-1415: Fix Use of Routing Connection Factory Resolves https://github.com/spring-projects/spring-amqp/issues/1415 The `sendConnectionFactorySelectorExpression` was being used for receive methods instead of `receiveConnectionFactorySelectorExpression`. **cherry-pick to 2.3.x, 2.2.x** --- .../amqp/rabbit/core/RabbitTemplate.java | 4 +-- .../amqp/rabbit/core/RabbitTemplateTests.java | 27 +++++++++++++------ 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java index ac4bd4b6f9..a5f030d1d5 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java @@ -1088,10 +1088,10 @@ private ConnectionFactory obtainTargetConnectionFactory(Expression expression, O (AbstractRoutingConnectionFactory) getConnectionFactory(); Object lookupKey; if (rootObject != null) { - lookupKey = this.sendConnectionFactorySelectorExpression.getValue(this.evaluationContext, rootObject); + lookupKey = expression.getValue(this.evaluationContext, rootObject); } else { - lookupKey = this.sendConnectionFactorySelectorExpression.getValue(this.evaluationContext); + lookupKey = expression.getValue(this.evaluationContext); } if (lookupKey != null) { ConnectionFactory connectionFactory = routingConnectionFactory.getTargetConnectionFactory(lookupKey); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateTests.java index c56bf2fc86..0c36a16228 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -371,24 +371,33 @@ public void testNoListenerAllowed2() { @SuppressWarnings("unchecked") public void testRoutingConnectionFactory() throws Exception { org.springframework.amqp.rabbit.connection.ConnectionFactory connectionFactory1 = - Mockito.mock(org.springframework.amqp.rabbit.connection.ConnectionFactory.class); + mock(org.springframework.amqp.rabbit.connection.ConnectionFactory.class); org.springframework.amqp.rabbit.connection.ConnectionFactory connectionFactory2 = - Mockito.mock(org.springframework.amqp.rabbit.connection.ConnectionFactory.class); + mock(org.springframework.amqp.rabbit.connection.ConnectionFactory.class); + org.springframework.amqp.rabbit.connection.ConnectionFactory connectionFactory3 = + mock(org.springframework.amqp.rabbit.connection.ConnectionFactory.class); + org.springframework.amqp.rabbit.connection.ConnectionFactory connectionFactory4 = + mock(org.springframework.amqp.rabbit.connection.ConnectionFactory.class); Map factories = new HashMap(2); factories.put("foo", connectionFactory1); factories.put("bar", connectionFactory2); + factories.put("baz", connectionFactory3); + factories.put("qux", connectionFactory4); AbstractRoutingConnectionFactory connectionFactory = new SimpleRoutingConnectionFactory(); connectionFactory.setTargetConnectionFactories(factories); final RabbitTemplate template = new RabbitTemplate(connectionFactory); - Expression expression = new SpelExpressionParser() + Expression sendExpression = new SpelExpressionParser() .parseExpression("T(org.springframework.amqp.rabbit.core.RabbitTemplateTests)" + ".LOOKUP_KEY_COUNT.getAndIncrement() % 2 == 0 ? 'foo' : 'bar'"); - template.setSendConnectionFactorySelectorExpression(expression); - template.setReceiveConnectionFactorySelectorExpression(expression); + template.setSendConnectionFactorySelectorExpression(sendExpression); + Expression receiveExpression = new SpelExpressionParser() + .parseExpression("T(org.springframework.amqp.rabbit.core.RabbitTemplateTests)" + + ".LOOKUP_KEY_COUNT.getAndIncrement() % 2 == 0 ? 'baz' : 'qux'"); + template.setReceiveConnectionFactorySelectorExpression(receiveExpression); for (int i = 0; i < 3; i++) { try { @@ -411,8 +420,10 @@ public void testRoutingConnectionFactory() throws Exception { } } - Mockito.verify(connectionFactory1, Mockito.times(5)).createConnection(); - Mockito.verify(connectionFactory2, Mockito.times(4)).createConnection(); + Mockito.verify(connectionFactory1, times(2)).createConnection(); + Mockito.verify(connectionFactory2, times(1)).createConnection(); + Mockito.verify(connectionFactory3, times(3)).createConnection(); + Mockito.verify(connectionFactory4, times(3)).createConnection(); } @Test From ce7d4b171d839b53943a725c7590517e589d7fed Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 4 Jan 2022 11:05:06 -0500 Subject: [PATCH 055/737] Upgrade Log4j to 2.17.1 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 36445f903d..19775f440b 100644 --- a/build.gradle +++ b/build.gradle @@ -52,7 +52,7 @@ ext { jaywayJsonPathVersion = '2.4.0' junit4Version = '4.13.2' junitJupiterVersion = '5.8.2' - log4jVersion = '2.17.0' + log4jVersion = '2.17.1' logbackVersion = '1.2.3' lz4Version = '1.8.0' micrometerVersion = '1.8.0' From b6466e2fcb8ac84d202292d75d9636c5915fd0bd Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 4 Jan 2022 14:46:24 -0500 Subject: [PATCH 056/737] Upgrade Gradle Kotlin Plugin --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 19775f440b..3d6a6472d4 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlinVersion = '1.4.32' + ext.kotlinVersion = '1.5.31' repositories { mavenCentral() maven { url 'https://plugins.gradle.org/m2' } @@ -394,7 +394,7 @@ project('spring-rabbit') { compileTestKotlin { kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '11' } } From 1a1b1f6c3c08c52affb9a9e7ca9e4e6965b1358b Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 3 Nov 2021 15:17:30 -0400 Subject: [PATCH 057/737] Main to 3.0; SF to 6.0; JDK 17 Initial Commit for Spring Framework 6.0/JDK 17 * Fix add-opens for java.util.zip; add distributionSha256Sum to gradle.properties. Fix remaining errors of migration to SF-6.0 * Remove `lineSeparator` property for `NewlineAtEndOfFile` Checkstyle rule: Windows checkouts files with a `crlf` line feed * Add `-Dfile.encoding=UTF-8` jmvarg for Gradle: some source files use non-UTF symbols * Upgrade to Kotlin `1.5.31`, JSON Path `2.6.0`, Junit `5.8.1` * Use correct links for external JavaDocs * Add `Xdoclint:syntax` to JavaDoc Gradle task to suppress JavaDoc warnings for classes * Use `DuplicatesStrategy.EXCLUDE` for `prepareAsciidocBuild` task * Align `api` task with SF style Add `Xdoclint:syntax` to `api` Gradle task Turns out `api` task generates fresh common JavaDocs, so it emits the same warnings for missed docs in classes Remove files restored by rebase Fix test Java version Fix build.gradle --- .github/workflows/pr-build-workflow.yml | 4 +- build.gradle | 52 ++-- gradle.properties | 3 +- .../client/AmqpClientInterceptor.java | 129 --------- .../remoting/client/AmqpProxyFactoryBean.java | 73 ----- .../amqp/remoting/client/package-info.java | 4 - .../service/AmqpInvokerServiceExporter.java | 135 ---------- .../amqp/remoting/service/package-info.java | 4 - ...nvocationAwareMessageConverterAdapter.java | 13 +- .../converter/RemoteInvocationResult.java | 157 +++++++++++ .../converter/RemoteInvocationUtils.java | 60 +++++ .../converter/SimpleMessageConverter.java | 47 +--- .../amqp/remoting/RemotingTest.java | 164 ----------- .../testhelper/AbstractAmqpTemplate.java | 255 ------------------ .../testhelper/SentSavingTemplate.java | 54 ---- .../testservice/GeneralException.java | 34 --- .../testservice/SpecialException.java | 34 --- .../remoting/testservice/TestServiceImpl.java | 62 ----- .../testservice/TestServiceInterface.java | 40 --- .../MessagingMessageListenerAdapter.java | 3 +- .../CachingConnectionFactoryTests.java | 5 +- .../core/BatchingRabbitTemplateTests.java | 2 +- .../amqp/rabbit/log4j2/AmqpAppenderTests.java | 11 +- .../log4j2/ExtendAmqpAppenderTests.java | 11 +- .../amqp/rabbit/remoting/RemotingTests.java | 124 --------- src/checkstyle/checkstyle.xml | 4 +- src/reference/asciidoc/amqp.adoc | 80 +----- src/reference/asciidoc/appendix.adoc | 15 ++ src/reference/asciidoc/whats-new.adoc | 20 +- 29 files changed, 309 insertions(+), 1290 deletions(-) delete mode 100644 spring-amqp/src/main/java/org/springframework/amqp/remoting/client/AmqpClientInterceptor.java delete mode 100644 spring-amqp/src/main/java/org/springframework/amqp/remoting/client/AmqpProxyFactoryBean.java delete mode 100644 spring-amqp/src/main/java/org/springframework/amqp/remoting/client/package-info.java delete mode 100644 spring-amqp/src/main/java/org/springframework/amqp/remoting/service/AmqpInvokerServiceExporter.java delete mode 100644 spring-amqp/src/main/java/org/springframework/amqp/remoting/service/package-info.java create mode 100644 spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationResult.java create mode 100644 spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationUtils.java delete mode 100644 spring-amqp/src/test/java/org/springframework/amqp/remoting/RemotingTest.java delete mode 100644 spring-amqp/src/test/java/org/springframework/amqp/remoting/testhelper/AbstractAmqpTemplate.java delete mode 100644 spring-amqp/src/test/java/org/springframework/amqp/remoting/testhelper/SentSavingTemplate.java delete mode 100644 spring-amqp/src/test/java/org/springframework/amqp/remoting/testservice/GeneralException.java delete mode 100644 spring-amqp/src/test/java/org/springframework/amqp/remoting/testservice/SpecialException.java delete mode 100644 spring-amqp/src/test/java/org/springframework/amqp/remoting/testservice/TestServiceImpl.java delete mode 100644 spring-amqp/src/test/java/org/springframework/amqp/remoting/testservice/TestServiceInterface.java delete mode 100644 spring-rabbit/src/test/java/org/springframework/amqp/rabbit/remoting/RemotingTests.java diff --git a/.github/workflows/pr-build-workflow.yml b/.github/workflows/pr-build-workflow.yml index 563aa47f78..5c1cb562d2 100644 --- a/.github/workflows/pr-build-workflow.yml +++ b/.github/workflows/pr-build-workflow.yml @@ -19,10 +19,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v1 with: - java-version: 11 + java-version: 17 - name: Run Gradle uses: burrunan/gradle-cache-action@v1 diff --git a/build.gradle b/build.gradle index 3d6a6472d4..92f189b0c0 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext.kotlinVersion = '1.5.31' repositories { mavenCentral() - maven { url 'https://plugins.gradle.org/m2' } + gradlePluginPortal() maven { url 'https://repo.spring.io/plugins-release' } } dependencies { @@ -49,7 +49,7 @@ ext { hamcrestVersion = '2.2' hibernateValidationVersion = '6.2.0.Final' jacksonBomVersion = '2.13.1' - jaywayJsonPathVersion = '2.4.0' + jaywayJsonPathVersion = '2.6.0' junit4Version = '4.13.2' junitJupiterVersion = '5.8.2' log4jVersion = '2.17.1' @@ -63,7 +63,7 @@ ext { reactorVersion = '2020.0.14' snappyVersion = '1.1.8.4' springDataCommonsVersion = '2.6.0' - springVersion = project.hasProperty('springVersion') ? project.springVersion : '5.3.14' + springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.0-SNAPSHOT' springRetryVersion = '1.3.1' zstdJniVersion = '1.5.0-2' } @@ -110,9 +110,9 @@ allprojects { ext { expandPlaceholders = '**/quick-tour.xml' javadocLinks = [ - 'https://docs.oracle.com/javase/8/docs/api/', - 'https://docs.oracle.com/javaee/7/api/', - 'https://docs.spring.io/spring/docs/current/javadoc-api/' + 'https://docs.oracle.com/en/java/javase/17/docs/api/', + 'https://jakarta.ee/specifications/platform/9/apidocs/', + 'https://docs.spring.io/spring-framework/docs/current/javadoc-api/' ] as String[] } @@ -140,13 +140,13 @@ subprojects { subproject -> } compileJava { - sourceCompatibility = 1.8 - targetCompatibility = 1.8 + sourceCompatibility = 17 + targetCompatibility = 17 } compileTestJava { - sourceCompatibility = 11 - targetCompatibility = 11 + sourceCompatibility = 17 + targetCompatibility = 17 options.encoding = 'UTF-8' } @@ -157,7 +157,7 @@ subprojects { subproject -> } jacoco { - toolVersion = '0.8.5' + toolVersion = '0.8.7' } // dependencies that are common across all java projects @@ -245,7 +245,16 @@ subprojects { subproject -> } } - compileKotlin.dependsOn updateCopyrights + compileKotlin.dependsOn updateCopyrights + + tasks.withType(JavaForkOptions) { + jvmArgs '--add-opens', 'java.base/java.util.zip=ALL-UNNAMED' + } + + tasks.withType(Javadoc) { + options.addBooleanOption('Xdoclint:syntax', true) // only check syntax with doclint + options.addBooleanOption('Werror', true) // fail build on Javadoc warnings + } test { maxHeapSize = '2g' @@ -394,7 +403,7 @@ project('spring-rabbit') { compileTestKotlin { kotlinOptions { - jvmTarget = '11' + jvmTarget = '16' } } @@ -553,12 +562,17 @@ task api(type: Javadoc) { group = 'Documentation' description = 'Generates aggregated Javadoc API documentation.' title = "${rootProject.description} ${version} API" - options.memberLevel = org.gradle.external.javadoc.JavadocMemberLevel.PROTECTED - options.author = true - options.header = rootProject.description - options.overview = 'src/api/overview.html' - options.stylesheetFile = file('src/api/stylesheet.css') - options.links(rootProject.ext.javadocLinks) + options { + encoding = 'UTF-8' + memberLevel = JavadocMemberLevel.PROTECTED + author = true + header = rootProject.description + use = true + overview = 'src/api/overview.html' + splitIndex = true + links(project.ext.javadocLinks) + addBooleanOption('Xdoclint:syntax', true) // only check syntax with doclint + } source subprojects.collect { project -> project.sourceSets.main.allJava diff --git a/gradle.properties b/gradle.properties index d9a850bc6e..f7e1c6ab57 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,5 @@ -version=2.4.2-SNAPSHOT +version=3.0.0-SNAPSHOT +org.gradle.jvmargs=-Xmx1536M -Dfile.encoding=UTF-8 org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g org.gradle.daemon=true diff --git a/spring-amqp/src/main/java/org/springframework/amqp/remoting/client/AmqpClientInterceptor.java b/spring-amqp/src/main/java/org/springframework/amqp/remoting/client/AmqpClientInterceptor.java deleted file mode 100644 index 6aeaa3177e..0000000000 --- a/spring-amqp/src/main/java/org/springframework/amqp/remoting/client/AmqpClientInterceptor.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2002-2019 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.remoting.client; - -import java.util.Arrays; - -import org.aopalliance.intercept.MethodInterceptor; -import org.aopalliance.intercept.MethodInvocation; - -import org.springframework.amqp.core.AmqpTemplate; -import org.springframework.remoting.RemoteProxyFailureException; -import org.springframework.remoting.support.DefaultRemoteInvocationFactory; -import org.springframework.remoting.support.RemoteAccessor; -import org.springframework.remoting.support.RemoteInvocation; -import org.springframework.remoting.support.RemoteInvocationFactory; -import org.springframework.remoting.support.RemoteInvocationResult; - -/** - * {@link org.aopalliance.intercept.MethodInterceptor} for accessing RMI-style AMQP services. - * - * @author David Bilge - * @author Gary Russell - * @since 1.2 - * @see org.springframework.amqp.remoting.service.AmqpInvokerServiceExporter - * @see AmqpProxyFactoryBean - * @see org.springframework.remoting.RemoteAccessException - * @deprecated will be removed in 3.0.0. - */ -@Deprecated -public class AmqpClientInterceptor extends RemoteAccessor implements MethodInterceptor { - - private AmqpTemplate amqpTemplate; - - private String routingKey = null; - - private RemoteInvocationFactory remoteInvocationFactory = new DefaultRemoteInvocationFactory(); - - @Override - public Object invoke(MethodInvocation invocation) throws Throwable { - RemoteInvocation remoteInvocation = getRemoteInvocationFactory().createRemoteInvocation(invocation); - - Object rawResult; - if (getRoutingKey() == null) { - // Use the template's default routing key - rawResult = this.amqpTemplate.convertSendAndReceive(remoteInvocation); - } - else { - rawResult = this.amqpTemplate.convertSendAndReceive(this.routingKey, remoteInvocation); - } - - if (rawResult == null) { - throw new RemoteProxyFailureException("No reply received from '" + - remoteInvocation.getMethodName() + - "' with arguments '" + - Arrays.asList(remoteInvocation.getArguments()) + // NOSONAR (null) - "' - perhaps a timeout in the template?", null); - } - else if (!(rawResult instanceof RemoteInvocationResult)) { - throw new RemoteProxyFailureException("Expected a result of type " - + RemoteInvocationResult.class.getCanonicalName() + " but found " - + rawResult.getClass().getCanonicalName(), null); // NOSONAR (null) - } - - RemoteInvocationResult result = (RemoteInvocationResult) rawResult; - return result.recreate(); - } - - public AmqpTemplate getAmqpTemplate() { - return this.amqpTemplate; - } - - /** - * The AMQP template to be used for sending messages and receiving results. This class is using "Request/Reply" for - * sending messages as described in the Spring-AMQP - * documentation. - * - * @param amqpTemplate The amqp template. - */ - public void setAmqpTemplate(AmqpTemplate amqpTemplate) { - this.amqpTemplate = amqpTemplate; - } - - public String getRoutingKey() { - return this.routingKey; - } - - /** - * The routing key to send calls to the service with. Use this to route the messages to a specific queue on the - * broker. If not set, the {@link AmqpTemplate}'s default routing key will be used. - *

- * This property is useful if you want to use the same AmqpTemplate to talk to multiple services. - * - * @param routingKey The routing key. - */ - public void setRoutingKey(String routingKey) { - this.routingKey = routingKey; - } - - public RemoteInvocationFactory getRemoteInvocationFactory() { - return this.remoteInvocationFactory; - } - - /** - * Set the RemoteInvocationFactory to use for this accessor. Default is a {@link DefaultRemoteInvocationFactory}. - *

- * A custom invocation factory can add further context information to the invocation, for example user credentials. - * - * @param remoteInvocationFactory The remote invocation factory. - */ - public void setRemoteInvocationFactory(RemoteInvocationFactory remoteInvocationFactory) { - this.remoteInvocationFactory = remoteInvocationFactory; - } - -} diff --git a/spring-amqp/src/main/java/org/springframework/amqp/remoting/client/AmqpProxyFactoryBean.java b/spring-amqp/src/main/java/org/springframework/amqp/remoting/client/AmqpProxyFactoryBean.java deleted file mode 100644 index 4ce946f470..0000000000 --- a/spring-amqp/src/main/java/org/springframework/amqp/remoting/client/AmqpProxyFactoryBean.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2002-2019 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.remoting.client; - -import org.springframework.aop.framework.ProxyFactory; -import org.springframework.beans.factory.FactoryBean; -import org.springframework.beans.factory.InitializingBean; - -/** - * {@link FactoryBean} for AMQP proxies. Exposes the proxied service for use as a bean reference, using the specified - * service interface. Proxies will throw Spring's unchecked RemoteAccessException on remote invocation failure. - * - *

- * This is intended for an "RMI-style" (i.e. synchroneous) usage of the AMQP protocol. Obviously, AMQP allows for a much - * broader scope of execution styles, which are not the scope of the mechanism at hand. - *

- * Calling a method on the proxy will cause an AMQP message being sent according to the configured - * {@link org.springframework.amqp.core.AmqpTemplate}. - * This can be received and answered by an {@link org.springframework.amqp.remoting.service.AmqpInvokerServiceExporter}. - * - * @author David Bilge - * @author Gary Russell - * - * @since 1.2 - * @see #setServiceInterface - * @see AmqpClientInterceptor - * @see org.springframework.remoting.rmi.RmiServiceExporter - * @see org.springframework.remoting.RemoteAccessException - * @deprecated will be removed in 3.0.0. - */ -@Deprecated -public class AmqpProxyFactoryBean extends AmqpClientInterceptor implements FactoryBean, InitializingBean { - - private Object serviceProxy; - - @Override - public void afterPropertiesSet() { - if (getServiceInterface() == null) { - throw new IllegalArgumentException("Property 'serviceInterface' is required"); - } - this.serviceProxy = new ProxyFactory(getServiceInterface(), this).getProxy(getBeanClassLoader()); - } - - @Override - public Object getObject() { - return this.serviceProxy; - } - - @Override - public Class getObjectType() { - return getServiceInterface(); - } - - @Override - public boolean isSingleton() { - return true; - } - -} diff --git a/spring-amqp/src/main/java/org/springframework/amqp/remoting/client/package-info.java b/spring-amqp/src/main/java/org/springframework/amqp/remoting/client/package-info.java deleted file mode 100644 index ac8ebec8ef..0000000000 --- a/spring-amqp/src/main/java/org/springframework/amqp/remoting/client/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Provides classes for the client side of Spring Remoting over AMQP. - */ -package org.springframework.amqp.remoting.client; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/remoting/service/AmqpInvokerServiceExporter.java b/spring-amqp/src/main/java/org/springframework/amqp/remoting/service/AmqpInvokerServiceExporter.java deleted file mode 100644 index 13817fde02..0000000000 --- a/spring-amqp/src/main/java/org/springframework/amqp/remoting/service/AmqpInvokerServiceExporter.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright 2002-2019 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.remoting.service; - -import org.springframework.amqp.AmqpRejectAndDontRequeueException; -import org.springframework.amqp.core.Address; -import org.springframework.amqp.core.AmqpTemplate; -import org.springframework.amqp.core.Message; -import org.springframework.amqp.core.MessageListener; -import org.springframework.amqp.core.MessageProperties; -import org.springframework.amqp.support.converter.MessageConverter; -import org.springframework.amqp.support.converter.SimpleMessageConverter; -import org.springframework.remoting.support.RemoteInvocation; -import org.springframework.remoting.support.RemoteInvocationBasedExporter; -import org.springframework.remoting.support.RemoteInvocationResult; - -/** - * This message listener exposes a plain java service via AMQP. Such services can be accessed via plain AMQP or via - * {@link org.springframework.amqp.remoting.client.AmqpProxyFactoryBean}. - * - * To configure this message listener so that it actually receives method calls via AMQP, it needs to be put into a - * listener container. See {@link MessageListener}. - * - *

- * When receiving a message, a service method is called according to the contained {@link RemoteInvocation}. The result - * of that invocation is returned as a {@link RemoteInvocationResult} contained in a message that is sent according to - * the ReplyToAddress of the received message. - * - *

- * Please note that this exporter does not use the {@link MessageConverter} of the injected {@link AmqpTemplate} to - * convert incoming calls and their results. Instead you have to directly inject the MessageConverter into - * this class. - * - *

- * This listener responds to "Request/Reply"-style messages as described here. - * - * @author David Bilge - * @author Gary Russell - * @author Artem Bilan - * @since 1.2 - * @deprecated will be removed in 3.0.0. - */ -@Deprecated -public class AmqpInvokerServiceExporter extends RemoteInvocationBasedExporter implements MessageListener { - - private AmqpTemplate amqpTemplate; - - private MessageConverter messageConverter = new SimpleMessageConverter(); - - @Override - public void onMessage(Message message) { - Address replyToAddress = message.getMessageProperties().getReplyToAddress(); - if (replyToAddress == null) { - throw new AmqpRejectAndDontRequeueException("No replyToAddress in inbound AMQP Message"); - } - - Object invocationRaw = this.messageConverter.fromMessage(message); - - RemoteInvocationResult remoteInvocationResult; - if (!(invocationRaw instanceof RemoteInvocation)) { - remoteInvocationResult = new RemoteInvocationResult( - new IllegalArgumentException("The message does not contain a RemoteInvocation payload")); - } - else { - RemoteInvocation invocation = (RemoteInvocation) invocationRaw; - remoteInvocationResult = invokeAndCreateResult(invocation, getService()); - } - send(remoteInvocationResult, replyToAddress, message); - } - - private void send(Object object, Address replyToAddress, Message requestMessage) { - Message message = this.messageConverter.toMessage(object, new MessageProperties()); - message.getMessageProperties().setCorrelationId(requestMessage.getMessageProperties().getCorrelationId()); - - getAmqpTemplate().send(replyToAddress.getExchangeName(), replyToAddress.getRoutingKey(), message); - } - - public AmqpTemplate getAmqpTemplate() { - return this.amqpTemplate; - } - - /** - * The AMQP template to use for sending the return value. - * - *

- * Note that the exchange and routing key parameters on this template are ignored for these return messages. Instead - * of those the respective parameters from the original message's returnAddress are being used. - *

- * Also, the template's {@link MessageConverter} is not used for the reply. - * - * @param amqpTemplate The amqp template. - * - * @see #setMessageConverter(MessageConverter) - */ - public void setAmqpTemplate(AmqpTemplate amqpTemplate) { - this.amqpTemplate = amqpTemplate; - } - - public MessageConverter getMessageConverter() { - return this.messageConverter; - } - - /** - * Set the message converter for this remote service. Used to deserialize remote method calls and to serialize their - * return values. - *

- * The default converter is a SimpleMessageConverter, which is able to handle byte arrays, Strings, and Serializable - * Objects depending on the message content type header. - *

- * Note that this class never uses the message converter of the underlying {@link AmqpTemplate}! - * - * @param messageConverter The message converter. - * - * @see org.springframework.amqp.support.converter.SimpleMessageConverter - */ - public void setMessageConverter(MessageConverter messageConverter) { - this.messageConverter = messageConverter; - } - -} diff --git a/spring-amqp/src/main/java/org/springframework/amqp/remoting/service/package-info.java b/spring-amqp/src/main/java/org/springframework/amqp/remoting/service/package-info.java deleted file mode 100644 index 5ec67d1a13..0000000000 --- a/spring-amqp/src/main/java/org/springframework/amqp/remoting/service/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Provides classes for the service side of Spring Remoting over AMQP. - */ -package org.springframework.amqp.remoting.service; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationAwareMessageConverterAdapter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationAwareMessageConverterAdapter.java index da0b8b8bfc..41199af6bc 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationAwareMessageConverterAdapter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationAwareMessageConverterAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2021 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. @@ -19,8 +19,6 @@ import org.springframework.amqp.AmqpRemoteException; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; -import org.springframework.beans.factory.BeanClassLoaderAware; -import org.springframework.remoting.support.RemoteInvocationResult; import org.springframework.util.Assert; /** @@ -32,7 +30,7 @@ * @since 2.0 * */ -public class RemoteInvocationAwareMessageConverterAdapter implements MessageConverter, BeanClassLoaderAware { +public class RemoteInvocationAwareMessageConverterAdapter implements MessageConverter { private final MessageConverter delegate; @@ -49,13 +47,6 @@ public RemoteInvocationAwareMessageConverterAdapter(MessageConverter delegate) { this.shouldSetClassLoader = false; } - @Override - public void setBeanClassLoader(ClassLoader classLoader) { - if (this.shouldSetClassLoader) { - ((SimpleMessageConverter) this.delegate).setBeanClassLoader(classLoader); - } - } - @Override public Message toMessage(Object object, MessageProperties messageProperties) throws MessageConversionException { return this.delegate.toMessage(object, messageProperties); diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationResult.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationResult.java new file mode 100644 index 0000000000..aba8346e01 --- /dev/null +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationResult.java @@ -0,0 +1,157 @@ +/* + * Copyright 2021 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.support.converter; + +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; + +import org.springframework.lang.Nullable; + +/** + * Encapsulates a remote invocation result, holding a result value or an exception. + * + * @author Juergen Hoeller + * @author Gary Russell + * @since 3.0 + */ +public class RemoteInvocationResult implements Serializable { + + /** Use serialVersionUID from Spring 1.1 for interoperability. */ + private static final long serialVersionUID = 2138555143707773549L; + + + @Nullable + private Object value; + + @Nullable + private Throwable exception; + + + /** + * Create a new RemoteInvocationResult for the given result value. + * @param value the result value returned by a successful invocation + * of the target method + */ + public RemoteInvocationResult(@Nullable Object value) { + this.value = value; + } + + /** + * Create a new RemoteInvocationResult for the given exception. + * @param exception the exception thrown by an unsuccessful invocation + * of the target method + */ + public RemoteInvocationResult(@Nullable Throwable exception) { + this.exception = exception; + } + + /** + * Create a new RemoteInvocationResult for JavaBean-style deserialization + * (e.g. with Jackson). + * @see #setValue + * @see #setException + */ + public RemoteInvocationResult() { + } + + + /** + * Set the result value returned by a successful invocation of the + * target method, if any. + *

This setter is intended for JavaBean-style deserialization. + * Use {@link #RemoteInvocationResult(Object)} otherwise. + * @see #RemoteInvocationResult() + */ + public void setValue(@Nullable Object value) { + this.value = value; + } + + /** + * Return the result value returned by a successful invocation + * of the target method, if any. + * @see #hasException + */ + @Nullable + public Object getValue() { + return this.value; + } + + /** + * Set the exception thrown by an unsuccessful invocation of the + * target method, if any. + *

This setter is intended for JavaBean-style deserialization. + * Use {@link #RemoteInvocationResult(Throwable)} otherwise. + * @see #RemoteInvocationResult() + */ + public void setException(@Nullable Throwable exception) { + this.exception = exception; + } + + /** + * Return the exception thrown by an unsuccessful invocation + * of the target method, if any. + * @see #hasException + */ + @Nullable + public Throwable getException() { + return this.exception; + } + + /** + * Return whether this invocation result holds an exception. + * If this returns {@code false}, the result value applies + * (even if it is {@code null}). + * @see #getValue + * @see #getException + */ + public boolean hasException() { + return (this.exception != null); + } + + /** + * Return whether this invocation result holds an InvocationTargetException, + * thrown by an invocation of the target method itself. + * @see #hasException() + */ + public boolean hasInvocationTargetException() { + return (this.exception instanceof InvocationTargetException); + } + + + /** + * Recreate the invocation result, either returning the result value + * in case of a successful invocation of the target method, or + * rethrowing the exception thrown by the target method. + * @return the result value, if any + * @throws Throwable the exception, if any + */ + @Nullable + public Object recreate() throws Throwable { + if (this.exception != null) { + Throwable exToThrow = this.exception; + if (this.exception instanceof InvocationTargetException) { + exToThrow = ((InvocationTargetException) this.exception).getTargetException(); + } + RemoteInvocationUtils.fillInClientStackTraceIfPossible(exToThrow); + throw exToThrow; + } + else { + return this.value; + } + } + +} diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationUtils.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationUtils.java new file mode 100644 index 0000000000..25aded8109 --- /dev/null +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationUtils.java @@ -0,0 +1,60 @@ +/* + * Copyright 2021 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.support.converter; + +import java.util.HashSet; +import java.util.Set; + +/** + * General utilities for handling remote invocations. + * + *

Mainly intended for use within the remoting framework. + * + * @author Juergen Hoeller + * @since 3.0 + */ +public abstract class RemoteInvocationUtils { + + /** + * Fill the current client-side stack trace into the given exception. + *

The given exception is typically thrown on the server and serialized + * as-is, with the client wanting it to contain the client-side portion + * of the stack trace as well. What we can do here is to update the + * {@code StackTraceElement} array with the current client-side stack + * trace, provided that we run on JDK 1.4+. + * @param ex the exception to update + * @see Throwable#getStackTrace() + * @see Throwable#setStackTrace(StackTraceElement[]) + */ + public static void fillInClientStackTraceIfPossible(Throwable ex) { + if (ex != null) { + StackTraceElement[] clientStack = new Throwable().getStackTrace(); + Set visitedExceptions = new HashSet<>(); + Throwable exToUpdate = ex; + while (exToUpdate != null && !visitedExceptions.contains(exToUpdate)) { + StackTraceElement[] serverStack = exToUpdate.getStackTrace(); + StackTraceElement[] combinedStack = new StackTraceElement[serverStack.length + clientStack.length]; + System.arraycopy(serverStack, 0, combinedStack, 0, serverStack.length); + System.arraycopy(clientStack, 0, combinedStack, serverStack.length, clientStack.length); + exToUpdate.setStackTrace(combinedStack); + visitedExceptions.add(exToUpdate); + exToUpdate = exToUpdate.getCause(); + } + } + } + +} diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SimpleMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SimpleMessageConverter.java index 732c339481..d162147d0c 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SimpleMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SimpleMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2021 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. @@ -27,8 +27,6 @@ import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.utils.SerializationUtils; -import org.springframework.beans.factory.BeanClassLoaderAware; -import org.springframework.util.ClassUtils; /** * Implementation of {@link MessageConverter} that can work with Strings, Serializable @@ -41,40 +39,12 @@ * @author Oleg Zhurakousky * @author Gary Russell */ -public class SimpleMessageConverter extends AllowedListDeserializingMessageConverter implements BeanClassLoaderAware { +public class SimpleMessageConverter extends AllowedListDeserializingMessageConverter { public static final String DEFAULT_CHARSET = "UTF-8"; private volatile String defaultCharset = DEFAULT_CHARSET; - private String codebaseUrl; - - private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); - - @Override - public void setBeanClassLoader(ClassLoader beanClassLoader) { - this.beanClassLoader = beanClassLoader; - } - - /** - * Set the codebase URL to download classes from if not found locally. Can consist of - * multiple URLs, separated by spaces. - *

- * Follows RMI's codebase conventions for dynamic class download. - * - * @param codebaseUrl The codebase URL. - * - * @deprecated due to deprecation of - * {@link org.springframework.remoting.rmi.CodebaseAwareObjectInputStream}. - * - * @see org.springframework.remoting.rmi.CodebaseAwareObjectInputStream - * @see java.rmi.server.RMIClassLoader - */ - @Deprecated - public void setCodebaseUrl(String codebaseUrl) { - this.codebaseUrl = codebaseUrl; - } - /** * Specify the default charset to use when converting to or from text-based * Message body content. If not specified, the charset will be "UTF-8". @@ -111,7 +81,7 @@ else if (contentType != null && contentType.equals(MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT)) { try { content = SerializationUtils.deserialize( - createObjectInputStream(new ByteArrayInputStream(message.getBody()), this.codebaseUrl)); + createObjectInputStream(new ByteArrayInputStream(message.getBody()))); } catch (IOException | IllegalArgumentException | IllegalStateException e) { throw new MessageConversionException( @@ -165,18 +135,15 @@ else if (object instanceof Serializable) { } /** - * Create an ObjectInputStream for the given InputStream and codebase. The default implementation creates a - * CodebaseAwareObjectInputStream. + * Create an ObjectInputStream for the given InputStream and codebase. The default + * implementation creates an ObjectInputStream. * @param is the InputStream to read from - * @param codebaseUrl the codebase URL to load classes from if not found locally (can be null) * @return the new ObjectInputStream instance to use * @throws IOException if creation of the ObjectInputStream failed - * @see org.springframework.remoting.rmi.CodebaseAwareObjectInputStream */ @SuppressWarnings("deprecation") - protected ObjectInputStream createObjectInputStream(InputStream is, String codebaseUrl) throws IOException { - return new org.springframework.remoting.rmi.CodebaseAwareObjectInputStream(is, this.beanClassLoader, - codebaseUrl) { + protected ObjectInputStream createObjectInputStream(InputStream is) throws IOException { + return new ObjectInputStream(is) { @Override protected Class resolveClass(ObjectStreamClass classDesc) throws IOException, ClassNotFoundException { diff --git a/spring-amqp/src/test/java/org/springframework/amqp/remoting/RemotingTest.java b/spring-amqp/src/test/java/org/springframework/amqp/remoting/RemotingTest.java deleted file mode 100644 index bf46a25092..0000000000 --- a/spring-amqp/src/test/java/org/springframework/amqp/remoting/RemotingTest.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright 2002-2019 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.remoting; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -import java.util.concurrent.atomic.AtomicBoolean; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.amqp.AmqpException; -import org.springframework.amqp.core.Address; -import org.springframework.amqp.core.AmqpTemplate; -import org.springframework.amqp.core.Message; -import org.springframework.amqp.core.MessageProperties; -import org.springframework.amqp.remoting.testhelper.AbstractAmqpTemplate; -import org.springframework.amqp.remoting.testhelper.SentSavingTemplate; -import org.springframework.amqp.remoting.testservice.GeneralException; -import org.springframework.amqp.remoting.testservice.SpecialException; -import org.springframework.amqp.remoting.testservice.TestServiceImpl; -import org.springframework.amqp.remoting.testservice.TestServiceInterface; -import org.springframework.amqp.support.converter.MessageConversionException; -import org.springframework.amqp.support.converter.MessageConverter; -import org.springframework.amqp.support.converter.SimpleMessageConverter; -import org.springframework.remoting.RemoteProxyFailureException; -import org.springframework.remoting.support.RemoteInvocation; - -/** - * @author David Bilge - * @author Artem Bilan - * @author Gary Russell - * @since 1.2 - */ -@SuppressWarnings("deprecation") -public class RemotingTest { - - private TestServiceInterface riggedProxy; - - private org.springframework.amqp.remoting.service.AmqpInvokerServiceExporter serviceExporter; - - /** - * Set up a rig of directly wired-up proxy and service listener so that both can be tested together without needing - * a running rabbit. - */ - @BeforeEach - public void initializeTestRig() { - // Set up the service - TestServiceInterface testService = new TestServiceImpl(); - this.serviceExporter = new org.springframework.amqp.remoting.service.AmqpInvokerServiceExporter(); - final SentSavingTemplate sentSavingTemplate = new SentSavingTemplate(); - this.serviceExporter.setAmqpTemplate(sentSavingTemplate); - this.serviceExporter.setService(testService); - this.serviceExporter.setServiceInterface(TestServiceInterface.class); - - // Set up the client - org.springframework.amqp.remoting.client.AmqpProxyFactoryBean amqpProxyFactoryBean = - new org.springframework.amqp.remoting.client.AmqpProxyFactoryBean(); - amqpProxyFactoryBean.setServiceInterface(TestServiceInterface.class); - AmqpTemplate directForwardingTemplate = new AbstractAmqpTemplate() { - @Override - public Object convertSendAndReceive(Object payload) throws AmqpException { - Object[] arguments = ((RemoteInvocation) payload).getArguments(); - if (arguments.length == 1 && arguments[0].equals("timeout")) { - return null; - } - - MessageConverter messageConverter = serviceExporter.getMessageConverter(); - - Address replyTo = new Address("fakeExchangeName", "fakeRoutingKey"); - MessageProperties messageProperties = new MessageProperties(); - messageProperties.setReplyToAddress(replyTo); - Message message = messageConverter.toMessage(payload, messageProperties); - - serviceExporter.onMessage(message); - - Message resultMessage = sentSavingTemplate.getLastMessage(); - return messageConverter.fromMessage(resultMessage); - } - }; - amqpProxyFactoryBean.setAmqpTemplate(directForwardingTemplate); - amqpProxyFactoryBean.afterPropertiesSet(); - Object rawProxy = amqpProxyFactoryBean.getObject(); - riggedProxy = (TestServiceInterface) rawProxy; - } - - @Test - public void testEcho() { - assertThat(riggedProxy.simpleStringReturningTestMethod("Test")).isEqualTo("Echo Test"); - } - - @Test - public void testSimulatedTimeout() { - try { - this.riggedProxy.simulatedTimeoutMethod("timeout"); - } - catch (RemoteProxyFailureException e) { - assertThat(e.getMessage()).contains("'simulatedTimeoutMethod' with arguments '[timeout]'"); - } - } - - @Test - public void testExceptionPropagation() { - assertThatExceptionOfType(AmqpException.class).isThrownBy(() -> riggedProxy.exceptionThrowingMethod()); - } - - @Test - public void testExceptionReturningMethod() { - assertThatExceptionOfType(GeneralException.class) - .isThrownBy(() -> riggedProxy.notReallyExceptionReturningMethod()); - } - - @Test - public void testActuallyExceptionReturningMethod() { - SpecialException returnedException = riggedProxy.actuallyExceptionReturningMethod(); - assertThat(returnedException).isNotNull(); - } - - @Test - public void testWrongRemoteInvocationArgument() { - MessageConverter messageConverter = this.serviceExporter.getMessageConverter(); - this.serviceExporter.setMessageConverter(new SimpleMessageConverter() { - - private final AtomicBoolean invoked = new AtomicBoolean(); - - @Override - protected Message createMessage(Object object, MessageProperties messageProperties) - throws MessageConversionException { - Message message = super.createMessage(object, messageProperties); - if (!invoked.getAndSet(true)) { - messageProperties.setContentType(null); - } - return message; - } - - }); - - try { - riggedProxy.simpleStringReturningTestMethod("Test"); - } - catch (Exception e) { - assertThat(e).isInstanceOf(IllegalArgumentException.class); - assertThat(e.getMessage()).contains("The message does not contain a RemoteInvocation payload"); - } - - this.serviceExporter.setMessageConverter(messageConverter); - } - -} diff --git a/spring-amqp/src/test/java/org/springframework/amqp/remoting/testhelper/AbstractAmqpTemplate.java b/spring-amqp/src/test/java/org/springframework/amqp/remoting/testhelper/AbstractAmqpTemplate.java deleted file mode 100644 index 4de10a97d8..0000000000 --- a/spring-amqp/src/test/java/org/springframework/amqp/remoting/testhelper/AbstractAmqpTemplate.java +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Copyright 2002-2019 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.remoting.testhelper; - -import org.springframework.amqp.AmqpException; -import org.springframework.amqp.core.AmqpTemplate; -import org.springframework.amqp.core.Message; -import org.springframework.amqp.core.MessagePostProcessor; -import org.springframework.amqp.core.ReceiveAndReplyCallback; -import org.springframework.amqp.core.ReplyToAddressCallback; -import org.springframework.core.ParameterizedTypeReference; - -/** - * @author David Bilge - * @author Ernest Sadykov - * @since 1.2 - */ -public abstract class AbstractAmqpTemplate implements AmqpTemplate { - - @Override - public void send(Message message) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public void send(String routingKey, Message message) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public void send(String exchange, String routingKey, Message message) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public void convertAndSend(Object message) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public void convertAndSend(String routingKey, Object message) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public void convertAndSend(String exchange, String routingKey, Object message) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public void convertAndSend(Object message, MessagePostProcessor messagePostProcessor) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public void convertAndSend(String routingKey, Object message, MessagePostProcessor messagePostProcessor) - throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public void convertAndSend(String exchange, String routingKey, Object message, - MessagePostProcessor messagePostProcessor) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Message receive() throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Message receive(String queueName) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Message receive(long timeoutMillis) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Message receive(String queueName, long timeoutMillis) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Object receiveAndConvert() throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Object receiveAndConvert(String queueName) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Object receiveAndConvert(long timeoutMillis) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Object receiveAndConvert(String queueName, long timeoutMillis) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public boolean receiveAndReply(ReceiveAndReplyCallback callback) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public boolean receiveAndReply(ReceiveAndReplyCallback callback, String exchange, String routingKey) - throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback, String replyExchange, - String replyRoutingKey) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public boolean receiveAndReply(ReceiveAndReplyCallback callback, ReplyToAddressCallback replyToAddressCallback) - throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback, - ReplyToAddressCallback replyToAddressCallback) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Message sendAndReceive(Message message) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Message sendAndReceive(String routingKey, Message message) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Message sendAndReceive(String exchange, String routingKey, Message message) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Object convertSendAndReceive(Object message) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Object convertSendAndReceive(String routingKey, Object message) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Object convertSendAndReceive(String exchange, String routingKey, Object message) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Object convertSendAndReceive(Object message, MessagePostProcessor messagePostProcessor) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Object convertSendAndReceive(String routingKey, Object message, MessagePostProcessor messagePostProcessor) - throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public Object convertSendAndReceive(String exchange, String routingKey, Object message, - MessagePostProcessor messagePostProcessor) throws AmqpException { - throw new UnsupportedOperationException(); - } - - @Override - public T convertSendAndReceiveAsType(final String exchange, final String routingKey, final Object message, final MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) - throws AmqpException { - return null; - } - - @Override - public T convertSendAndReceiveAsType(final String routingKey, final Object message, final MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) - throws AmqpException { - return null; - } - - @Override - public T convertSendAndReceiveAsType(final Object message, final MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) throws AmqpException { - return null; - } - - @Override - public T convertSendAndReceiveAsType(final String exchange, final String routingKey, final Object message, ParameterizedTypeReference responseType) - throws AmqpException { - return null; - } - - @Override - public T convertSendAndReceiveAsType(final String routingKey, final Object message, ParameterizedTypeReference responseType) throws AmqpException { - return null; - } - - @Override - public T convertSendAndReceiveAsType(final Object message, ParameterizedTypeReference responseType) throws AmqpException { - return null; - } - - @Override - public T receiveAndConvert(String queueName, long timeoutMillis, ParameterizedTypeReference type) throws AmqpException { - return null; - } - - @Override - public T receiveAndConvert(long timeoutMillis, ParameterizedTypeReference type) throws AmqpException { - return null; - } - - @Override - public T receiveAndConvert(String queueName, ParameterizedTypeReference type) throws AmqpException { - return null; - } - - @Override - public T receiveAndConvert(ParameterizedTypeReference type) throws AmqpException { - return null; - } - -} diff --git a/spring-amqp/src/test/java/org/springframework/amqp/remoting/testhelper/SentSavingTemplate.java b/spring-amqp/src/test/java/org/springframework/amqp/remoting/testhelper/SentSavingTemplate.java deleted file mode 100644 index eb6dfe3979..0000000000 --- a/spring-amqp/src/test/java/org/springframework/amqp/remoting/testhelper/SentSavingTemplate.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2002-2019 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.remoting.testhelper; - -import org.springframework.amqp.AmqpException; -import org.springframework.amqp.core.Message; - -/** - * @author David Bilge - * @author Gary Russell - * @since 1.2 - */ -public class SentSavingTemplate extends AbstractAmqpTemplate { - - private Message lastMessage = null; - - private String lastExchange = null; - - private String lastRoutingKey = null; - - @Override - public void send(String exchange, String routingKey, Message message) throws AmqpException { - this.lastExchange = exchange; - this.lastRoutingKey = routingKey; - this.lastMessage = message; - } - - public Message getLastMessage() { - return lastMessage; - } - - public String getLastExchange() { - return lastExchange; - } - - public String getLastRoutingKey() { - return lastRoutingKey; - } - -} diff --git a/spring-amqp/src/test/java/org/springframework/amqp/remoting/testservice/GeneralException.java b/spring-amqp/src/test/java/org/springframework/amqp/remoting/testservice/GeneralException.java deleted file mode 100644 index 7466e8384e..0000000000 --- a/spring-amqp/src/test/java/org/springframework/amqp/remoting/testservice/GeneralException.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2002-2019 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.remoting.testservice; - -/** - * @author David Bilge - * @since 1.2 - */ -public class GeneralException extends RuntimeException { - private static final long serialVersionUID = 1763252570120227426L; - - public GeneralException(String message, Throwable cause) { - super(message, cause); - } - - public GeneralException(String message) { - super(message); - } - -} diff --git a/spring-amqp/src/test/java/org/springframework/amqp/remoting/testservice/SpecialException.java b/spring-amqp/src/test/java/org/springframework/amqp/remoting/testservice/SpecialException.java deleted file mode 100644 index e8a9de7102..0000000000 --- a/spring-amqp/src/test/java/org/springframework/amqp/remoting/testservice/SpecialException.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2002-2019 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.remoting.testservice; - -/** - * @author David Bilge - * @since 1.2 - */ -public class SpecialException extends RuntimeException { - private static final long serialVersionUID = 7254934411128057730L; - - public SpecialException(String message, Throwable cause) { - super(message, cause); - } - - public SpecialException(String message) { - super(message); - } - -} diff --git a/spring-amqp/src/test/java/org/springframework/amqp/remoting/testservice/TestServiceImpl.java b/spring-amqp/src/test/java/org/springframework/amqp/remoting/testservice/TestServiceImpl.java deleted file mode 100644 index 6e758d6536..0000000000 --- a/spring-amqp/src/test/java/org/springframework/amqp/remoting/testservice/TestServiceImpl.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2002-2019 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.remoting.testservice; - -import org.springframework.amqp.AmqpException; - -/** - * @author David Bilge - * @author Gary Russell - * @since 1.2 - */ -public class TestServiceImpl implements TestServiceInterface { - @Override - public void simpleTestMethod() { - // Do nothing - } - - @Override - public String simpleStringReturningTestMethod(String string) { - return "Echo " + string; - } - - @Override - public void exceptionThrowingMethod() { - throw new AmqpException("This is an exception"); - } - - @Override - public Object echo(Object o) { - return o; - } - - @Override - public SpecialException notReallyExceptionReturningMethod() { - throw new GeneralException("This exception should not be interpreted as a return type but be thrown instead."); - } - - @Override - public SpecialException actuallyExceptionReturningMethod() { - return new SpecialException("This exception should not be thrown on the client side but just be returned!"); - } - - @Override - public Object simulatedTimeoutMethod(Object o) { - return null; - } - -} diff --git a/spring-amqp/src/test/java/org/springframework/amqp/remoting/testservice/TestServiceInterface.java b/spring-amqp/src/test/java/org/springframework/amqp/remoting/testservice/TestServiceInterface.java deleted file mode 100644 index a5b78a3eef..0000000000 --- a/spring-amqp/src/test/java/org/springframework/amqp/remoting/testservice/TestServiceInterface.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2002-2019 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.remoting.testservice; - -/** - * @author David Bilge - * @author Gary Russell - * @since 1.2 - */ -public interface TestServiceInterface { - - void simpleTestMethod(); - - String simpleStringReturningTestMethod(String string); - - void exceptionThrowingMethod(); - - Object echo(Object o); - - SpecialException notReallyExceptionReturningMethod(); - - SpecialException actuallyExceptionReturningMethod(); - - Object simulatedTimeoutMethod(Object o); - -} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java index fc585a1b1b..4084a6ba94 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java @@ -30,6 +30,7 @@ import org.springframework.amqp.support.converter.MessageConversionException; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.amqp.support.converter.MessagingMessageConverter; +import org.springframework.amqp.support.converter.RemoteInvocationResult; import org.springframework.core.MethodParameter; import org.springframework.lang.Nullable; import org.springframework.messaging.Message; @@ -38,7 +39,6 @@ import org.springframework.messaging.handler.annotation.Headers; import org.springframework.messaging.handler.annotation.Payload; import org.springframework.messaging.support.MessageBuilder; -import org.springframework.remoting.support.RemoteInvocationResult; import org.springframework.util.Assert; import com.rabbitmq.client.Channel; @@ -221,7 +221,6 @@ private void returnOrThrow(org.springframework.amqp.core.Message amqpMessage, Ch } Object payload = message == null ? null : message.getPayload(); try { - handleResult(new InvocationResult(new RemoteInvocationResult(throwableToReturn), null, payload == null ? Object.class : this.handlerAdapter.getReturnTypeFor(payload), this.handlerAdapter.getBean(), diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java index 380994f869..7534b46565 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -34,7 +34,6 @@ import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -1763,7 +1762,7 @@ public void testOrderlyShutDown() throws Exception { AtomicBoolean rejected = new AtomicBoolean(true); CountDownLatch closeLatch = new CountDownLatch(1); ccf.setPublisherChannelFactory((channel, exec) -> { - executor.set(spy(exec)); + executor.set(exec); return pcc; }); willAnswer(invoc -> { diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplateTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplateTests.java index 12cf466696..60138e9045 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplateTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * Copyright 2014-2021 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. diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/AmqpAppenderTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/AmqpAppenderTests.java index 6f4afb5779..c81b48890b 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/AmqpAppenderTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/AmqpAppenderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2021 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. @@ -38,6 +38,7 @@ import org.apache.logging.log4j.core.LoggerContext; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.BindingBuilder; @@ -123,6 +124,7 @@ public void test() { } @Test + @Disabled("weird - this.events.take() in appender is returning null") public void testProperties() { Logger logger = LogManager.getLogger("foo"); AmqpAppender appender = (AmqpAppender) TestUtils.getPropertyValue(logger, "context.configuration.appenders", @@ -165,7 +167,12 @@ public void testProperties() { // default value assertThat(TestUtils.getPropertyValue(manager, "addMdcAsHeaders", Boolean.class)).isTrue(); - assertThat(TestUtils.getPropertyValue(appender, "events.items", Object[].class).length).isEqualTo(10); + java.util.Queue queue = TestUtils.getPropertyValue(appender, "events", java.util.Queue.class); + int i = 0; + while (queue.poll() != null) { + i++; + } + assertThat(i).isEqualTo(10); Object events = TestUtils.getPropertyValue(appender, "events"); assertThat(events.getClass()).isEqualTo(ArrayBlockingQueue.class); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/ExtendAmqpAppenderTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/ExtendAmqpAppenderTests.java index a9d76fa568..7e8530675d 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/ExtendAmqpAppenderTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/ExtendAmqpAppenderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2021 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. @@ -33,6 +33,7 @@ import org.apache.logging.log4j.core.LoggerContext; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.BindingBuilder; @@ -115,6 +116,7 @@ public void test() { } @Test + @Disabled("weird - this.events.take() in appender is returning null") public void testProperties() { Logger logger = LogManager.getLogger("foo"); AmqpAppender appender = (AmqpAppender) TestUtils.getPropertyValue(logger, "context.configuration.appenders", @@ -159,7 +161,12 @@ public void testProperties() { // default value assertThat(TestUtils.getPropertyValue(manager, "addMdcAsHeaders", Boolean.class)).isTrue(); - assertThat(TestUtils.getPropertyValue(appender, "events.items", Object[].class).length).isEqualTo(10); + java.util.Queue queue = TestUtils.getPropertyValue(appender, "events", java.util.Queue.class); + int i = 0; + while (queue.poll() != null) { + i++; + } + assertThat(i).isEqualTo(0); assertThat(TestUtils.getPropertyValue(appender, "foo")).isEqualTo("foo"); assertThat(TestUtils.getPropertyValue(appender, "bar")).isEqualTo("bar"); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/remoting/RemotingTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/remoting/RemotingTests.java deleted file mode 100644 index 43c940b099..0000000000 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/remoting/RemotingTests.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2002-2019 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.remoting; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; -import org.springframework.amqp.rabbit.core.RabbitAdmin; -import org.springframework.amqp.rabbit.junit.RabbitAvailable; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.remoting.RemoteProxyFailureException; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; - -/** - * @author Gary Russell - * @since 1.2 - * - */ -@SpringJUnitConfig -@DirtiesContext -@RabbitAvailable -public class RemotingTests { - - @Autowired - private ServiceInterface client; - - private static CountDownLatch latch; - - private static String receivedMessage; - - @BeforeAll - @AfterAll - public static void setupAndCleanUp() { - CachingConnectionFactory cf = new CachingConnectionFactory("localhost"); - RabbitAdmin admin = new RabbitAdmin(cf); - admin.deleteExchange("remoting.test.exchange"); - admin.deleteQueue("remoting.test.queue"); - cf.destroy(); - } - - @Test - public void testEcho() { - String reply = client.echo("foo"); - assertThat(reply).isEqualTo("echo:foo"); - } - - @Test - public void testNoAnswer() throws Exception { - latch = new CountDownLatch(1); - client.noAnswer("foo"); - assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(receivedMessage).isEqualTo("received:foo"); - } - - @Test - public void testTimeout() { - try { - client.suspend(); - fail("Exception expected"); - } - catch (RemoteProxyFailureException e) { - assertThat(e.getMessage()).contains(" - perhaps a timeout in the template?"); - } - } - - public interface ServiceInterface { - - String echo(String message); - - void noAnswer(String message); - - void suspend(); - - } - - public static class ServiceImpl implements ServiceInterface { - - @Override - public String echo(String message) { - return "echo:" + message; - } - - @Override - public void noAnswer(String message) { - receivedMessage = "received:" + message; - latch.countDown(); - } - - @Override - public void suspend() { - try { - Thread.sleep(3000); - } - catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - - - } -} diff --git a/src/checkstyle/checkstyle.xml b/src/checkstyle/checkstyle.xml index ff50870bf9..57b294ffdc 100644 --- a/src/checkstyle/checkstyle.xml +++ b/src/checkstyle/checkstyle.xml @@ -13,9 +13,7 @@ - - - + diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index d76a45cd4d..b944e452d1 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -4584,85 +4584,9 @@ See <> for more information. [[remoting]] ===== Spring Remoting with AMQP -IMPORTANT: This feature is deprecated and will be removed in 3.0. -It has been superseded for a long time by <> with the `returnExceptions` being set to true, and configuring a `RemoteInvocationAwareMessageConverterAdapter` on the sending side. -See <> for more information. +Spring remoting is no longer supported because the functionality has been removed from Spring Framework. -The Spring Framework has a general remoting capability, allowing https://docs.spring.io/spring/docs/current/spring-framework-reference/html/remoting.html[Remote Procedure Calls (RPC)] that use various transports. -Spring-AMQP supports a similar mechanism with a `AmqpProxyFactoryBean` on the client and a `AmqpInvokerServiceExporter` on the server. -This provides RPC over AMQP. -On the client side, a `RabbitTemplate` is used as described <>. -On the server side, the invoker (configured as a `MessageListener`) receives the message, invokes the configured service, and returns the reply by using the inbound message's `replyTo` information. - -You can inject the client factory bean into any bean (by using its `serviceInterface`). -The client can then invoke methods on the proxy, resulting in remote execution over AMQP. - -NOTE: With the default `MessageConverter` instances, the method parameters and returned value must be instances of `Serializable`. - -On the server side, the `AmqpInvokerServiceExporter` has both `AmqpTemplate` and `MessageConverter` properties. -Currently, the template's `MessageConverter` is not used. -If you need to supply a custom message converter, you should provide it by setting the `messageConverter` property. -On the client side, you can add a custom message converter to the `AmqpTemplate`, which is provided to the `AmqpProxyFactoryBean` by using its `amqpTemplate` property. - -The following listing shows sample client and server configurations: - -==== -[source,xml] ----- - - - - - - - - - - - - - - - - - - ----- - -[source,xml] ----- - - - - - - - - - - - - - - - - - ----- -==== - -IMPORTANT: The `AmqpInvokerServiceExporter` can process only properly formed messages, such as those sent from the `AmqpProxyFactoryBean`. -If it receives a message that it cannot interpret, a serialized `RuntimeException` is sent as a reply. -If the message has no `replyToAddress` property, the message is rejected and permanently lost if no dead letter exchange has been configured. - -NOTE: By default, if the request message cannot be delivered, the calling thread eventually times out and a `RemoteProxyFailureException` is thrown. -By default, the timeout is five seconds. -You can modify that duration by setting the `replyTimeout` property on the `RabbitTemplate`. -Starting with version 1.5, by setting the `mandatory` property to `true` and enabling returns on the connection factory (see <>), the calling thread throws an `AmqpMessageReturnedException`. -See <> for more information. +Use `sendAndReceive` operations using the `RabbitTemplate` (client side ) and `@RabbitListener` instead. [[broker-configuration]] ==== Configuring the Broker diff --git a/src/reference/asciidoc/appendix.adoc b/src/reference/asciidoc/appendix.adoc index 859e175f3b..18cc3a2889 100644 --- a/src/reference/asciidoc/appendix.adoc +++ b/src/reference/asciidoc/appendix.adoc @@ -10,6 +10,21 @@ See <>. [[previous-whats-new]] === Previous Releases +==== Changes in 2.4 Since 2.3 + +This section describes the changes between version 2.3 and version 2.4. +See <> for changes in previous versions. + +===== `@RabbitListener` Changes + +`MessageProperties` is now available for argument matching. +See <> for more information. + +===== `RabbitAdmin` Changes + +A new property `recoverManualDeclarations` allows recovery of manually declared queues/exchanges/bindings. +See <> for more information. + ==== Changes in 2.3 Since 2.2 This section describes the changes between version 2.2 and version 2.3. diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index 5e6673f42a..b9d83a054b 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -1,22 +1,14 @@ [[whats-new]] == What's New -=== Changes in 2.4 Since 2.3 +=== Changes in 3.0 Since 2.4 -This section describes the changes between version 2.4 and version 2.4. -See <> for changes in previous versions. +==== Java 17, Spring Framework 6.0 -==== `@RabbitListener` Changes +This version requires Spring Framework 6.0 and Java 17 -`MessageProperties` is now available for argument matching. -See <> for more information. +==== Remoting -==== `RabbitAdmin` Changes +The remoting feature (using RMI) is no longer supported. -A new property `recoverManualDeclarations` allows recovery of manually declared queues/exchanges/bindings. -See <> for more information. - -==== Remoting Support - -Support remoting using Spring Framework's RMI support is deprecated and will be removed in 3.0. -See <> for more information. +See <> for alternatives. From 006b018171705d1a0bfd1390ea9ecfdf534cee46 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Sun, 16 Jan 2022 10:11:21 -0500 Subject: [PATCH 058/737] Upgrade Versions; Prepare for Milestone Release --- build.gradle | 8 ++++---- .../rabbit/listener/AbstractMessageListenerContainer.java | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 92f189b0c0..0184a2ee03 100644 --- a/build.gradle +++ b/build.gradle @@ -55,15 +55,15 @@ ext { log4jVersion = '2.17.1' logbackVersion = '1.2.3' lz4Version = '1.8.0' - micrometerVersion = '1.8.0' + micrometerVersion = '2.0.0-M1' mockitoVersion = '3.11.2' rabbitmqStreamVersion = '0.4.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.13.1' rabbitmqHttpClientVersion = '3.12.1' - reactorVersion = '2020.0.14' + reactorVersion = '2020.0.15' snappyVersion = '1.1.8.4' - springDataCommonsVersion = '2.6.0' - springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.0-SNAPSHOT' + springDataCommonsVersion = '3.0.0-M1' + springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.0-M2' springRetryVersion = '1.3.1' zstdJniVersion = '1.5.0-2' } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index 374f227902..10b191e5fc 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -2139,6 +2139,7 @@ Object start() { return Timer.start(this.registry); } + @SuppressWarnings("deprecation") void success(Object sample, String queue) { Timer timer = this.timers.get(queue + "none"); if (timer == null) { @@ -2147,6 +2148,7 @@ void success(Object sample, String queue) { ((Sample) sample).stop(timer); } + @SuppressWarnings("deprecation") void failure(Object sample, String queue, String exception) { Timer timer = this.timers.get(queue + exception); if (timer == null) { From a43ebe43b00ad9de633c8d7d6e46dd71b9548692 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Sun, 16 Jan 2022 15:29:35 +0000 Subject: [PATCH 059/737] [artifactory-release] Release version 3.0.0-M1 --- gradle.properties | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index f7e1c6ab57..e18baea640 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,4 @@ -version=3.0.0-SNAPSHOT -org.gradle.jvmargs=-Xmx1536M -Dfile.encoding=UTF-8 +version=3.0.0-M1 org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g org.gradle.daemon=true From 2494cfb75f1a5d576bbb5b870cf39f3be3fe717e Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Sun, 16 Jan 2022 15:29:37 +0000 Subject: [PATCH 060/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index e18baea640..17d1d06efb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.0-M1 +version=3.0.0-SNAPSHOT org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g org.gradle.daemon=true From c27d9664ee35900fd9d3d5f81708bc715d38d376 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 18 Jan 2022 12:20:39 -0500 Subject: [PATCH 061/737] Restore file.encoding --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 17d1d06efb..2477c6a3c5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ version=3.0.0-SNAPSHOT org.gradlee.caching=true -org.gradle.jvmargs=-Xms512m -Xmx4g +org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.parallel=true kotlin.stdlib.default.dependency=false From 4309ff328dd74ce011aae9f7f85278cc5bbce24a Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 1 Feb 2022 11:35:06 -0500 Subject: [PATCH 062/737] Remove declareCollections from Doc Incorrectly left over after the property was removed in 2.2. --- src/reference/asciidoc/amqp.adoc | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index b944e452d1..1475262971 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -5094,8 +5094,6 @@ public static class Config { IMPORTANT: In versions prior to 2.1, you could declare multiple `Declarable` instances by defining beans of type `Collection`. This can cause undesirable side effects in some cases, because the admin has to iterate over all `Collection` beans. -This feature is now disabled in favor of `Declarables`, as discussed earlier in this section. -You can revert to the previous behavior by setting the `RabbitAdmin` property called `declareCollections` to `true`. Version 2.2 added the `getDeclarablesByType` method to `Declarables`; this can be used as a convenience, for example, when declaring the listener container bean(s). From 49bc52d4fd073e12aa908145a87228bd5af02f36 Mon Sep 17 00:00:00 2001 From: Muhammad Hewedy Date: Thu, 3 Feb 2022 12:19:52 +0300 Subject: [PATCH 063/737] fix multi-rabbit example RoutingConnectionFactory is not a ConnectionFactory --- src/reference/asciidoc/amqp.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index 1475262971..f095701266 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -6728,7 +6728,7 @@ public class Application { } @Bean - RabbitTemplate template(RoutingConnectionFactory rcf) { + RabbitTemplate template(SimpleRoutingConnectionFactory rcf) { return new RabbitTemplate(rcf); } From 8b2cc61f7e2c1fa486f80f7974a4512b12e55425 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 14 Feb 2022 12:13:40 -0500 Subject: [PATCH 064/737] GH-1422: @RabbitListener: Fix Broker-Named Queues Resolves https://github.com/spring-projects/spring-amqp/issues/1422 Broker-named queues did not work with `@RabbitListener` because the BPP only passed the name of the queue bean, not the bean itself, into the endpoint. Support bean injection; fall back to the previous behavior if a mixture of beans and names are encountered (the containers don't support both types of configuration). Add a note to the javadoc to indicate that broker-named queues are not supported via `queuesToDeclare` and `bindings` properties; such queues must be declared as discrete beans. * Docs. **cherry-pick to `2.4.x` & `2.3.x`** --- .../rabbit/annotation/RabbitListener.java | 6 +- ...itListenerAnnotationBeanPostProcessor.java | 71 ++++++++++++++----- .../EnableRabbitIntegrationTests.java | 20 +++++- ...tenerAnnotationBeanPostProcessorTests.java | 14 ++-- src/reference/asciidoc/amqp.adoc | 3 +- 5 files changed, 88 insertions(+), 26 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListener.java index 59a0cd9941..51c43847c4 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListener.java @@ -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. @@ -148,6 +148,8 @@ * application context, the queue will be declared on the broker with default * binding (default exchange with the queue name as the routing key). * Mutually exclusive with {@link #bindings()} and {@link #queues()}. + * NOTE: Broker-named queues cannot be declared this way, they must be defined + * as beans (with an empty string for the name). * @return the queue(s) to declare. * @see org.springframework.amqp.rabbit.listener.MessageListenerContainer * @since 2.0 @@ -186,6 +188,8 @@ * Array of {@link QueueBinding}s providing the listener's queue names, together * with the exchange and optional binding information. * Mutually exclusive with {@link #queues()} and {@link #queuesToDeclare()}. + * NOTE: Broker-named queues cannot be declared this way, they must be defined + * as beans (with an empty string for the name). * @return the bindings. * @see org.springframework.amqp.rabbit.listener.MessageListenerContainer * @since 1.5 diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java index dc6e5f4fd6..74983a7320 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 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. @@ -414,7 +414,19 @@ protected Collection processListener(MethodRabbitListenerEndpoint en endpoint.setBean(bean); endpoint.setMessageHandlerMethodFactory(this.messageHandlerMethodFactory); endpoint.setId(getEndpointId(rabbitListener)); - endpoint.setQueueNames(resolveQueues(rabbitListener, declarables)); + List resolvedQueues = resolveQueues(rabbitListener, declarables); + if (!resolvedQueues.isEmpty()) { + if (resolvedQueues.get(0) instanceof String) { + endpoint.setQueueNames(resolvedQueues.stream() + .map(o -> (String) o) + .collect(Collectors.toList()).toArray(new String[0])); + } + else { + endpoint.setQueues(resolvedQueues.stream() + .map(o -> (Queue) o) + .collect(Collectors.toList()).toArray(new Queue[0])); + } + } endpoint.setConcurrency(resolveExpressionAsStringOrInteger(rabbitListener.concurrency(), "concurrency")); endpoint.setBeanFactory(this.beanFactory); endpoint.setReturnExceptions(resolveExpressionAsBoolean(rabbitListener.returnExceptions())); @@ -625,23 +637,29 @@ private String getEndpointId(RabbitListener rabbitListener) { } } - private String[] resolveQueues(RabbitListener rabbitListener, Collection declarables) { + private List resolveQueues(RabbitListener rabbitListener, Collection declarables) { String[] queues = rabbitListener.queues(); QueueBinding[] bindings = rabbitListener.bindings(); org.springframework.amqp.rabbit.annotation.Queue[] queuesToDeclare = rabbitListener.queuesToDeclare(); - List result = new ArrayList(); + List queueNames = new ArrayList(); + List queueBeans = new ArrayList(); if (queues.length > 0) { for (int i = 0; i < queues.length; i++) { - resolveAsString(resolveExpression(queues[i]), result, true, "queues"); + resolveQueues(queues[i], queueNames, queueBeans); } } + if (!queueNames.isEmpty()) { + // revert to the previous behavior of just using the name when there is mixture of String and Queue + queueBeans.forEach(qb -> queueNames.add(qb.getName())); + queueBeans.clear(); + } if (queuesToDeclare.length > 0) { if (queues.length > 0) { throw new BeanInitializationException( "@RabbitListener can have only one of 'queues', 'queuesToDeclare', or 'bindings'"); } for (int i = 0; i < queuesToDeclare.length; i++) { - result.add(declareQueue(queuesToDeclare[i], declarables)); + queueNames.add(declareQueue(queuesToDeclare[i], declarables)); } } if (bindings.length > 0) { @@ -649,26 +667,47 @@ private String[] resolveQueues(RabbitListener rabbitListener, Collection (Object) s) + .collect(Collectors.toList()); + } + return queueNames.isEmpty() + ? queueBeans.stream() + .map(s -> (Object) s) + .collect(Collectors.toList()) + : queueNames.stream() + .map(s -> (Object) s) + .collect(Collectors.toList()); + + } + + private void resolveQueues(String queue, List result, List queueBeans) { + resolveAsStringOrQueue(resolveExpression(queue), result, queueBeans, "queues"); } @SuppressWarnings("unchecked") - private void resolveAsString(Object resolvedValue, List result, boolean canBeQueue, String what) { + private void resolveAsStringOrQueue(Object resolvedValue, List names, @Nullable List queues, + String what) { + Object resolvedValueToUse = resolvedValue; if (resolvedValue instanceof String[]) { resolvedValueToUse = Arrays.asList((String[]) resolvedValue); } - if (canBeQueue && resolvedValueToUse instanceof Queue) { - result.add(((Queue) resolvedValueToUse).getName()); + if (queues != null && resolvedValueToUse instanceof Queue) { + if (!names.isEmpty()) { + // revert to the previous behavior of just using the name when there is mixture of String and Queue + names.add(((Queue) resolvedValueToUse).getName()); + } + else { + queues.add((Queue) resolvedValueToUse); + } } else if (resolvedValueToUse instanceof String) { - result.add((String) resolvedValueToUse); + names.add((String) resolvedValueToUse); } else if (resolvedValueToUse instanceof Iterable) { for (Object object : (Iterable) resolvedValueToUse) { - resolveAsString(object, result, canBeQueue, what); + resolveAsStringOrQueue(object, names, queues, what); } } else { @@ -676,7 +715,7 @@ else if (resolvedValueToUse instanceof Iterable) { "@RabbitListener." + what + " can't resolve '%s' as a String[] or a String " - + (canBeQueue ? "or a Queue" : ""), + + (queues != null ? "or a Queue" : ""), resolvedValue)); } } @@ -776,7 +815,7 @@ private void registerBindings(QueueBinding binding, String queueName, String exc final int length = binding.key().length; routingKeys = new ArrayList<>(); for (int i = 0; i < length; ++i) { - resolveAsString(resolveExpression(binding.key()[i]), routingKeys, false, "@QueueBinding.key"); + resolveAsStringOrQueue(resolveExpression(binding.key()[i]), routingKeys, null, "@QueueBinding.key"); } } final Map bindingArguments = resolveArguments(binding.arguments()); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java index 277f2b5ed5..850cf1104e 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 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. @@ -60,6 +60,7 @@ import org.springframework.amqp.core.MessagePostProcessor; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.core.MessagePropertiesBuilder; +import org.springframework.amqp.core.QueueBuilder; import org.springframework.amqp.rabbit.config.DirectRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerEndpoint; @@ -74,6 +75,7 @@ import org.springframework.amqp.rabbit.junit.LogLevels; import org.springframework.amqp.rabbit.junit.RabbitAvailable; import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; +import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; import org.springframework.amqp.rabbit.listener.ConditionalRejectingErrorHandler; import org.springframework.amqp.rabbit.listener.DirectMessageListenerContainer; import org.springframework.amqp.rabbit.listener.MessageListenerContainer; @@ -1017,6 +1019,13 @@ void messagePropertiesParam() { })).isEqualTo("foo, myProp=bar"); } + @Test + void listenerWithBrokerNamedQueue() { + AbstractMessageListenerContainer container = + (AbstractMessageListenerContainer) this.registry.getListenerContainer("brokerNamed"); + assertThat(container.getQueueNames()[0]).startsWith("amq.gen"); + } + interface TxService { @Transactional @@ -1419,6 +1428,10 @@ public String mpArgument(String payload, MessageProperties props) { return payload + ", myProp=" + props.getHeader("myProp"); } + @RabbitListener(id = "brokerNamed", queues = "#{@brokerNamed}") + void brokerNamed(String in) { + } + } public static class JsonObject { @@ -2010,6 +2023,11 @@ public ReplyPostProcessor echoPrefixHeader() { }; } + @Bean + org.springframework.amqp.core.Queue brokerNamed() { + return QueueBuilder.nonDurable("").autoDelete().exclusive().build(); + } + } @RabbitListener(bindings = @QueueBinding diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessorTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessorTests.java index b7a06e28bc..e58b7cbaa9 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessorTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -187,9 +187,9 @@ public void multipleQueuesTestBean() { RabbitListenerContainerTestFactory factory = context.getBean(RabbitListenerContainerTestFactory.class); assertThat(factory.getListenerContainers().size()).as("one container should have been registered").isEqualTo(1); RabbitListenerEndpoint endpoint = factory.getListenerContainers().get(0).getEndpoint(); - final Iterator iterator = ((AbstractRabbitListenerEndpoint) endpoint).getQueueNames().iterator(); - assertThat(iterator.next()).isEqualTo("testQueue"); - assertThat(iterator.next()).isEqualTo("secondQueue"); + final Iterator iterator = ((AbstractRabbitListenerEndpoint) endpoint).getQueues().iterator(); + assertThat(iterator.next().getName()).isEqualTo("testQueue"); + assertThat(iterator.next().getName()).isEqualTo("secondQueue"); context.close(); } @@ -218,9 +218,9 @@ public void propertyResolvingToExpressionTestBean() { RabbitListenerContainerTestFactory factory = context.getBean(RabbitListenerContainerTestFactory.class); assertThat(factory.getListenerContainers().size()).as("one container should have been registered").isEqualTo(1); RabbitListenerEndpoint endpoint = factory.getListenerContainers().get(0).getEndpoint(); - final Iterator iterator = ((AbstractRabbitListenerEndpoint) endpoint).getQueueNames().iterator(); - assertThat(iterator.next()).isEqualTo("testQueue"); - assertThat(iterator.next()).isEqualTo("secondQueue"); + final Iterator iterator = ((AbstractRabbitListenerEndpoint) endpoint).getQueues().iterator(); + assertThat(iterator.next().getName()).isEqualTo("testQueue"); + assertThat(iterator.next().getName()).isEqualTo("secondQueue"); context.close(); } diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index f095701266..05675d1b1e 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -2340,7 +2340,8 @@ public class MyService { In the first example, a queue `myQueue` is declared automatically (durable) together with the exchange, if needed, and bound to the exchange with the routing key. -In the second example, an anonymous (exclusive, auto-delete) queue is declared and bound. +In the second example, 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; see <>. Multiple `QueueBinding` entries can be provided, letting the listener listen to multiple queues. In the third example, 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. From a3aedc67072bdfc01850dec7f87195cd8bf86381 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 17 Feb 2022 15:59:39 -0500 Subject: [PATCH 065/737] Revise Issue Template etc. --- .github/ISSUE_TEMPLATE.md | 27 ----------------- .github/ISSUE_TEMPLATE/bug_report.md | 36 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 5 ++++ .github/ISSUE_TEMPLATE/feature_request.md | 25 ++++++++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 3 +- src/dist/license.txt => LICENSE.txt | 0 README.md | 10 ++++++- build.gradle | 12 ++++++-- 8 files changed, 86 insertions(+), 32 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md rename src/dist/license.txt => LICENSE.txt (100%) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 58356963ad..0000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,27 +0,0 @@ - -**Affects Version(s):** \ - ---- - diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..cfab0e3ba9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,36 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: 'type: bug, status: waiting-for-triage' +assignees: '' + +--- + +**In what version(s) of Spring AMQP are you seeing this issue?** + +For example: + +2.4.2 + +Between 2.3.0 and 2.4.2 + +**Describe the bug** + +A clear and concise description of what the bug is. +Do not create an issue to ask a question; see below. + +**To Reproduce** + +Steps to reproduce the behavior. + +**Expected behavior** + +A clear and concise description of what you expected to happen. + +**Sample** + +A link to a GitHub repository with a [minimal, reproducible, sample](https://stackoverflow.com/help/minimal-reproducible-example). + +Reports that include a sample will take priority over reports that do not. +At times, we may require a sample, so it is good to try and include a sample up front. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..bddfd341d4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Community Support + url: https://stackoverflow.com/questions/tagged/spring-amqp + about: Please ask and answer questions on StackOverflow with the tag spring-amqp, or use the Discussions tab above diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..faa6dff469 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,25 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: 'status: waiting-for-triage, type: enhancement' +assignees: '' + +--- + +**Expected Behavior** + + + +**Current Behavior** + + + +**Context** + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f232d0d7b0..eb37328f9a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,5 +1,6 @@ diff --git a/src/dist/license.txt b/LICENSE.txt similarity index 100% rename from src/dist/license.txt rename to LICENSE.txt diff --git a/README.md b/README.md index cc8d252965..8dc67cf47d 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,14 @@ Spring AMQP [ Date: Mon, 7 Mar 2022 12:47:42 -0500 Subject: [PATCH 066/737] Remove Invalid Deprecation Suppressions Resolves https://github.com/spring-projects/spring-amqp/issues/1418 `Sample.stop(Timer)` is no longer deprecated. --- build.gradle | 3 ++- .../amqp/rabbit/listener/AbstractMessageListenerContainer.java | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 6cf2f05521..5b6a11023f 100644 --- a/build.gradle +++ b/build.gradle @@ -55,7 +55,7 @@ ext { log4jVersion = '2.17.1' logbackVersion = '1.2.3' lz4Version = '1.8.0' - micrometerVersion = '2.0.0-M1' + micrometerVersion = '2.0.0-SNAPSHOT' mockitoVersion = '3.11.2' rabbitmqStreamVersion = '0.4.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.13.1' @@ -382,6 +382,7 @@ project('spring-rabbit') { optionalApi "ch.qos.logback:logback-classic:$logbackVersion" optionalApi 'org.apache.logging.log4j:log4j-core' optionalApi "io.micrometer:micrometer-core:$micrometerVersion" + optionalApi "io.micrometer:micrometer-binders:$micrometerVersion" // Spring Data projection message binding support optionalApi ("org.springframework.data:spring-data-commons:$springDataCommonsVersion") { exclude group: 'org.springframework' diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index 10b191e5fc..374f227902 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -2139,7 +2139,6 @@ Object start() { return Timer.start(this.registry); } - @SuppressWarnings("deprecation") void success(Object sample, String queue) { Timer timer = this.timers.get(queue + "none"); if (timer == null) { @@ -2148,7 +2147,6 @@ void success(Object sample, String queue) { ((Sample) sample).stop(timer); } - @SuppressWarnings("deprecation") void failure(Object sample, String queue, String exception) { Timer timer = this.timers.get(queue + exception); if (timer == null) { From 1cbdb7de4784cb0310083f0b6d3b991de516c032 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 9 Mar 2022 13:03:39 -0500 Subject: [PATCH 067/737] GH-1251: Jackson2JsonMessageConverter Improvements Resolves https://github.com/spring-projects/spring-amqp/issues/1420 - detect and use `charset` in `contentType` when present - allow Jackson to determine the decode `charset` via `ByteSourceJsonBootstrapper.detectEncoding()` - allow configuration of the `MimeType` to use, which can include a `charset` parameter **cherry-pick to main - will require what's new fix** * Fix typo in doc. # Conflicts: # src/reference/asciidoc/whats-new.adoc --- .../AbstractJackson2MessageConverter.java | 93 ++++++++++++++++--- .../Jackson2JsonMessageConverterTests.java | 65 ++++++++++++- src/reference/asciidoc/amqp.adoc | 23 +++++ src/reference/asciidoc/whats-new.adoc | 5 +- 4 files changed, 170 insertions(+), 16 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java index 8f3e11315e..5c102712d1 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2020 the original author or authors. + * Copyright 2018-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. @@ -33,6 +33,7 @@ import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; @@ -61,13 +62,18 @@ public abstract class AbstractJackson2MessageConverter extends AbstractMessageCo */ public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + protected final ObjectMapper objectMapper; // NOSONAR protected + /** - * The supported content type; only the subtype is checked, e.g. */json, - * */xml. + * The supported content type; only the subtype is checked when decoding, e.g. + * */json, */xml. If this contains a charset parameter, when encoding, the + * contentType header will not be set, when decoding, the raw bytes are passed to + * Jackson which can dynamically determine the encoding; otherwise the contentEncoding + * or default charset is used. */ - private final MimeType supportedContentType; + private MimeType supportedContentType; - protected final ObjectMapper objectMapper; // NOSONAR protected + private String supportedCTCharset; @Nullable private ClassMapper classMapper = null; @@ -93,8 +99,11 @@ public abstract class AbstractJackson2MessageConverter extends AbstractMessageCo /** * Construct with the provided {@link ObjectMapper} instance. * @param objectMapper the {@link ObjectMapper} to use. - * @param contentType supported content type when decoding messages, only the subtype - * is checked, e.g. */json, */xml. + * @param contentType the supported content type; only the subtype is checked when + * decoding, e.g. */json, */xml. If this contains a charset parameter, when + * encoding, the contentType header will not be set, when decoding, the raw bytes are + * passed to Jackson which can dynamically determine the encoding; otherwise the + * contentEncoding or default charset is used. * @param trustedPackages the trusted Java packages for deserialization * @see DefaultJackson2JavaTypeMapper#setTrustedPackages(String...) */ @@ -105,9 +114,41 @@ protected AbstractJackson2MessageConverter(ObjectMapper objectMapper, MimeType c Assert.notNull(contentType, "'contentType' must not be null"); this.objectMapper = objectMapper; this.supportedContentType = contentType; + this.supportedCTCharset = this.supportedContentType.getParameter("charset"); ((DefaultJackson2JavaTypeMapper) this.javaTypeMapper).setTrustedPackages(trustedPackages); } + + /** + * Get the supported content type; only the subtype is checked when decoding, e.g. + * */json, */xml. If this contains a charset parameter, when encoding, the + * contentType header will not be set, when decoding, the raw bytes are passed to + * Jackson which can dynamically determine the encoding; otherwise the contentEncoding + * or default charset is used. + * @return the supportedContentType + * @since 2.4.3 + */ + protected MimeType getSupportedContentType() { + return this.supportedContentType; + } + + + /** + * Set the supported content type; only the subtype is checked when decoding, e.g. + * */json, */xml. If this contains a charset parameter, when encoding, the + * contentType header will not be set, when decoding, the raw bytes are passed to + * Jackson which can dynamically determine the encoding; otherwise the contentEncoding + * or default charset is used. + * @param supportedContentType the supportedContentType to set. + * @since 2.4.3 + */ + public void setSupportedContentType(MimeType supportedContentType) { + Assert.notNull(supportedContentType, "'supportedContentType' cannot be null"); + this.supportedContentType = supportedContentType; + this.supportedCTCharset = this.supportedContentType.getParameter("charset"); + } + + @Nullable public ClassMapper getClassMapper() { return this.classMapper; @@ -264,10 +305,7 @@ public Object fromMessage(Message message, @Nullable Object conversionHint) thro if ((this.assumeSupportedContentType // NOSONAR Boolean complexity && (contentType == null || contentType.equals(MessageProperties.DEFAULT_CONTENT_TYPE))) || (contentType != null && contentType.contains(this.supportedContentType.getSubtype()))) { - String encoding = properties.getContentEncoding(); - if (encoding == null) { - encoding = getDefaultCharset(); - } + String encoding = determineEncoding(properties, contentType); content = doFromMessage(message, conversionHint, properties, encoding); } else { @@ -283,6 +321,24 @@ public Object fromMessage(Message message, @Nullable Object conversionHint) thro return content; } + private String determineEncoding(MessageProperties properties, @Nullable String contentType) { + String encoding = properties.getContentEncoding(); + if (encoding == null && contentType != null) { + try { + MimeType mimeType = MimeTypeUtils.parseMimeType(contentType); + if (mimeType != null) { + encoding = mimeType.getParameter("charset"); + } + } + catch (RuntimeException e) { + } + } + if (encoding == null) { + encoding = this.supportedCTCharset != null ? this.supportedCTCharset : getDefaultCharset(); + } + return encoding; + } + private Object doFromMessage(Message message, Object conversionHint, MessageProperties properties, String encoding) { @@ -348,11 +404,17 @@ private Object tryConverType(Message message, String encoding, JavaType inferred } private Object convertBytesToObject(byte[] body, String encoding, JavaType targetJavaType) throws IOException { + if (this.supportedCTCharset != null) { // Jackson will determine encoding + return this.objectMapper.readValue(body, targetJavaType); + } String contentAsString = new String(body, encoding); return this.objectMapper.readValue(contentAsString, targetJavaType); } private Object convertBytesToObject(byte[] body, String encoding, Class targetClass) throws IOException { + if (this.supportedCTCharset != null) { // Jackson will determine encoding + return this.objectMapper.readValue(body, this.objectMapper.constructType(targetClass)); + } String contentAsString = new String(body, encoding); return this.objectMapper.readValue(contentAsString, this.objectMapper.constructType(targetClass)); } @@ -370,20 +432,23 @@ protected Message createMessage(Object objectToConvert, MessageProperties messag byte[] bytes; try { - if (this.charsetIsUtf8) { + if (this.charsetIsUtf8 && this.supportedCTCharset == null) { bytes = this.objectMapper.writeValueAsBytes(objectToConvert); } else { String jsonString = this.objectMapper .writeValueAsString(objectToConvert); - bytes = jsonString.getBytes(getDefaultCharset()); + String encoding = this.supportedCTCharset != null ? this.supportedCTCharset : getDefaultCharset(); + bytes = jsonString.getBytes(encoding); } } catch (IOException e) { throw new MessageConversionException("Failed to convert Message content", e); } messageProperties.setContentType(this.supportedContentType.toString()); - messageProperties.setContentEncoding(getDefaultCharset()); + if (this.supportedCTCharset == null) { + messageProperties.setContentEncoding(getDefaultCharset()); + } messageProperties.setContentLength(bytes.length); if (getClassMapper() == null) { diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverterTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverterTests.java index 19ec20ea4b..37e274d314 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverterTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -17,6 +17,7 @@ package org.springframework.amqp.support.converter; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import java.io.IOException; import java.math.BigDecimal; @@ -34,6 +35,7 @@ import org.springframework.core.ParameterizedTypeReference; import org.springframework.data.web.JsonPath; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.util.MimeTypeUtils; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; @@ -399,6 +401,67 @@ void concreteInMapRegression() throws Exception { assertThat(foos.values().iterator().next().getField()).isEqualTo("baz"); } + @Test + void charsetInContentType() { + trade.setUserName("John Doe ∫"); + Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter(); + String utf8 = "application/json;charset=utf-8"; + converter.setSupportedContentType(MimeTypeUtils.parseMimeType(utf8)); + Message message = converter.toMessage(trade, new MessageProperties()); + int bodyLength8 = message.getBody().length; + assertThat(message.getMessageProperties().getContentEncoding()).isNull(); + assertThat(message.getMessageProperties().getContentType()).isEqualTo(utf8); + SimpleTrade marshalledTrade = (SimpleTrade) converter.fromMessage(message); + assertThat(marshalledTrade).isEqualTo(trade); + + // use content type property + String utf16 = "application/json;charset=utf-16"; + converter.setSupportedContentType(MimeTypeUtils.parseMimeType(utf16)); + message = converter.toMessage(trade, new MessageProperties()); + assertThat(message.getBody().length).isNotEqualTo(bodyLength8); + assertThat(message.getMessageProperties().getContentEncoding()).isNull(); + assertThat(message.getMessageProperties().getContentType()).isEqualTo(utf16); + marshalledTrade = (SimpleTrade) converter.fromMessage(message); + assertThat(marshalledTrade).isEqualTo(trade); + + // no encoding in message, use configured default + converter.setSupportedContentType(MimeTypeUtils.parseMimeType("application/json")); + converter.setDefaultCharset("UTF-16"); + message = converter.toMessage(trade, new MessageProperties()); + assertThat(message.getBody().length).isNotEqualTo(bodyLength8); + assertThat(message.getMessageProperties().getContentEncoding()).isNotNull(); + message.getMessageProperties().setContentEncoding(null); + marshalledTrade = (SimpleTrade) converter.fromMessage(message); + assertThat(marshalledTrade).isEqualTo(trade); + + } + + @Test + void noConfigForCharsetInContentType() { + trade.setUserName("John Doe ∫"); + Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter(); + Message message = converter.toMessage(trade, new MessageProperties()); + int bodyLength8 = message.getBody().length; + SimpleTrade marshalledTrade = (SimpleTrade) converter.fromMessage(message); + assertThat(marshalledTrade).isEqualTo(trade); + + // no encoding in message; use configured default + message = converter.toMessage(trade, new MessageProperties()); + assertThat(message.getMessageProperties().getContentEncoding()).isNotNull(); + message.getMessageProperties().setContentEncoding(null); + marshalledTrade = (SimpleTrade) converter.fromMessage(message); + assertThat(marshalledTrade).isEqualTo(trade); + + converter.setDefaultCharset("UTF-16"); + Message message2 = converter.toMessage(trade, new MessageProperties()); + message2.getMessageProperties().setContentEncoding(null); + assertThat(message2.getBody().length).isNotEqualTo(bodyLength8); + converter.setDefaultCharset("UTF-8"); + + assertThatExceptionOfType(MessageConversionException.class).isThrownBy( + () -> converter.fromMessage(message2)); + } + public List fooLister() { return null; } diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index 05675d1b1e..9c40006343 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -3938,11 +3938,34 @@ public DefaultClassMapper classMapper() { Now, if the sending system sets the header to `thing1`, the converter creates a `Thing1` object, and so on. See the <> sample application for a complete discussion about converting messages from non-Spring applications. +Starting with version 2.4.3, the converter will not add a `contentEncoding` message property if the `supportedMediaType` has a `charset` parameter; this is also used for the encoding. +A new method `setSupportedMediaType` has been added: + +==== +[source, java] +---- +String utf16 = "application/json; charset=utf-16"; +converter.setSupportedContentType(MimeTypeUtils.parseMimeType(utf16)); +---- +==== + [[Jackson2JsonMessageConverter-from-message]] ====== Converting from a `Message` Inbound messages are converted to objects according to the type information added to headers by the sending system. +Starting with version 2.4.3, if there is no `contentEncoding` message property, the converter will attempt to detect a `charset` parameter in the `contentType` message property and use that. +If neither exist, if the `supportedMediaType` has a `charset` parameter, it will be used for decoding, with a final fallback to the `defaultCharset` property. +A new method `setSupportedMediaType` has been added: + +==== +[source, java] +---- +String utf16 = "application/json; charset=utf-16"; +converter.setSupportedContentType(MimeTypeUtils.parseMimeType(utf16)); +---- +==== + In versions prior to 1.6, if type information is not present, conversion would fail. Starting with version 1.6, if type information is missing, the converter converts the JSON by using Jackson defaults (usually a map). diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index b9d83a054b..d216446a8c 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -11,4 +11,7 @@ This version requires Spring Framework 6.0 and Java 17 The remoting feature (using RMI) is no longer supported. -See <> for alternatives. +==== Message Converter Changes + +The `Jackson2JsonMessageConverter` can now determine the charset from the `contentEncoding` header. +See <> for more information. \ No newline at end of file From cdf1a9cda0a962758b4ebe21ffeff90a80277d59 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 9 Mar 2022 13:28:44 -0500 Subject: [PATCH 068/737] GH-1420: Move 2.4.x What's New to Appendix Previous commit had wrong GH. --- src/reference/asciidoc/appendix.adoc | 9 +++++++++ src/reference/asciidoc/whats-new.adoc | 5 ----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/reference/asciidoc/appendix.adoc b/src/reference/asciidoc/appendix.adoc index 18cc3a2889..d1f3bf6d2b 100644 --- a/src/reference/asciidoc/appendix.adoc +++ b/src/reference/asciidoc/appendix.adoc @@ -25,6 +25,15 @@ See <> for more information. A new property `recoverManualDeclarations` allows recovery of manually declared queues/exchanges/bindings. See <> for more information. +===== Remoting Support + +Support remoting using Spring Framework’s RMI support is deprecated and will be removed in 3.0. See Spring Remoting with AMQP for more information. + +==== Message Converter Changes + +The `Jackson2JsonMessageConverter` can now determine the charset from the `contentEncoding` header. +See <> for more information. + ==== Changes in 2.3 Since 2.2 This section describes the changes between version 2.2 and version 2.3. diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index d216446a8c..fb15a9e091 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -10,8 +10,3 @@ This version requires Spring Framework 6.0 and Java 17 ==== Remoting The remoting feature (using RMI) is no longer supported. - -==== Message Converter Changes - -The `Jackson2JsonMessageConverter` can now determine the charset from the `contentEncoding` header. -See <> for more information. \ No newline at end of file From 909ba571bb096ed8acf77fed43e944f7b8ebb471 Mon Sep 17 00:00:00 2001 From: Leonardo Ferreira Date: Wed, 9 Mar 2022 11:58:15 -0300 Subject: [PATCH 069/737] Fix PooledChannelConnectionFactory Dont set defaultPublisherFactory = false when calling from constructor --- .../PooledChannelConnectionFactory.java | 5 +- .../PooledChannelConnectionFactoryTests.java | 104 +++++++++++++++++- 2 files changed, 106 insertions(+), 3 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java index 7187264860..b3ad7c5471 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-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. @@ -50,6 +50,7 @@ * a callback. * * @author Gary Russell + * @author Leonardo Ferreira * @since 2.3 * */ @@ -79,7 +80,7 @@ public PooledChannelConnectionFactory(ConnectionFactory rabbitConnectionFactory) private PooledChannelConnectionFactory(ConnectionFactory rabbitConnectionFactory, boolean isPublisher) { super(rabbitConnectionFactory); if (!isPublisher) { - setPublisherConnectionFactory(new PooledChannelConnectionFactory(rabbitConnectionFactory, true)); + doSetPublisherConnectionFactory(new PooledChannelConnectionFactory(rabbitConnectionFactory, true)); } else { this.defaultPublisherFactory = false; diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactoryTests.java index 469f086f05..da3eb6886f 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-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. @@ -19,6 +19,7 @@ import static org.assertj.core.api.Assertions.assertThat; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Test; @@ -31,12 +32,14 @@ import org.springframework.context.annotation.Configuration; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.test.util.ReflectionTestUtils; import com.rabbitmq.client.Channel; import com.rabbitmq.client.ConnectionFactory; /** * @author Gary Russell + * @author Leonardo Ferreira * @since 2.3 * */ @@ -97,6 +100,105 @@ void queueDeclared(@Autowired RabbitAdmin admin, @Autowired Config config, assertThat(config.closed).isTrue(); } + @Test + void copyConfigsToPublisherConnectionFactory() { + PooledChannelConnectionFactory pcf = new PooledChannelConnectionFactory(new ConnectionFactory()); + AtomicInteger txConfiged = new AtomicInteger(); + AtomicInteger nonTxConfiged = new AtomicInteger(); + pcf.setPoolConfigurer((pool, tx) -> { + if (tx) { + txConfiged.incrementAndGet(); + } + else { + nonTxConfiged.incrementAndGet(); + } + }); + + createAndCloseConnectionChannelTxAndChannelNonTx(pcf); + + final org.springframework.amqp.rabbit.connection.ConnectionFactory publisherConnectionFactory = pcf + .getPublisherConnectionFactory(); + assertThat(publisherConnectionFactory).isNotNull(); + + createAndCloseConnectionChannelTxAndChannelNonTx(publisherConnectionFactory); + + assertThat(txConfiged.get()).isEqualTo(2); + assertThat(nonTxConfiged.get()).isEqualTo(2); + + final Object listenerPoolConfigurer = ReflectionTestUtils.getField(pcf, "poolConfigurer"); + final Object publisherPoolConfigurer = ReflectionTestUtils.getField(publisherConnectionFactory, + "poolConfigurer"); + + assertThat(listenerPoolConfigurer) + .isSameAs(publisherPoolConfigurer); + + pcf.destroy(); + } + + @Test + void copyConfigsToPublisherConnectionFactoryWhenUsingCustomPublisherFactory() { + PooledChannelConnectionFactory pcf = new PooledChannelConnectionFactory(new ConnectionFactory()); + AtomicBoolean listenerTxConfiged = new AtomicBoolean(); + AtomicBoolean listenerNonTxConfiged = new AtomicBoolean(); + pcf.setPoolConfigurer((pool, tx) -> { + if (tx) { + listenerTxConfiged.set(true); + } + else { + listenerNonTxConfiged.set(true); + } + }); + + final PooledChannelConnectionFactory publisherConnectionFactory = new PooledChannelConnectionFactory( + new ConnectionFactory()); + + AtomicBoolean publisherTxConfiged = new AtomicBoolean(); + AtomicBoolean publisherNonTxConfiged = new AtomicBoolean(); + publisherConnectionFactory.setPoolConfigurer((pool, tx) -> { + if (tx) { + publisherTxConfiged.set(true); + } + else { + publisherNonTxConfiged.set(true); + } + }); + + pcf.setPublisherConnectionFactory(publisherConnectionFactory); + + assertThat(pcf.getPublisherConnectionFactory()).isSameAs(publisherConnectionFactory); + + createAndCloseConnectionChannelTxAndChannelNonTx(pcf); + + assertThat(listenerTxConfiged.get()).isEqualTo(true); + assertThat(listenerNonTxConfiged.get()).isEqualTo(true); + + final Object listenerPoolConfigurer = ReflectionTestUtils.getField(pcf, "poolConfigurer"); + final Object publisherPoolConfigurer = ReflectionTestUtils.getField(publisherConnectionFactory, + "poolConfigurer"); + + assertThat(listenerPoolConfigurer) + .isNotSameAs(publisherPoolConfigurer); + + createAndCloseConnectionChannelTxAndChannelNonTx(publisherConnectionFactory); + + assertThat(publisherTxConfiged.get()).isEqualTo(true); + assertThat(publisherNonTxConfiged.get()).isEqualTo(true); + + pcf.destroy(); + } + + private void createAndCloseConnectionChannelTxAndChannelNonTx( + org.springframework.amqp.rabbit.connection.ConnectionFactory connectionFactory) { + + Connection connection = connectionFactory.createConnection(); + Channel nonTxChannel = connection.createChannel(false); + Channel txChannel = connection.createChannel(true); + + RabbitUtils.closeChannel(nonTxChannel); + RabbitUtils.closeChannel(txChannel); + connection.close(); + } + @Configuration public static class Config { From a1aba732ed3f0ac3efafcc187aa48122a67c8e1f Mon Sep 17 00:00:00 2001 From: Leonardo Ferreira Date: Tue, 15 Mar 2022 12:45:54 -0300 Subject: [PATCH 070/737] Fix eviction logic in the PooledChannelCF The method `destroyObject()` is called by the eviction process of Apache Pool2, but it tries to do a logical close that puts the channel back on the pool causing a `java.lang.IllegalStateException: Returned object not currently part of this pool and object lost (abandoned).` * Extract the `targetChannel` from the proxy and call a `physicalClose()` in the `destroyObject()` for a proper pool eviction **Cherry-pick to `2.4.x` & `2.3.x`** --- .../amqp/rabbit/connection/ChannelProxy.java | 7 +- .../PooledChannelConnectionFactory.java | 7 +- .../PooledChannelConnectionFactoryTests.java | 68 +++++++++++++++++++ 3 files changed, 79 insertions(+), 3 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ChannelProxy.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ChannelProxy.java index 8e394b8eea..17a423b7ba 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ChannelProxy.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ChannelProxy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -16,6 +16,8 @@ package org.springframework.amqp.rabbit.connection; +import org.springframework.aop.RawTargetAccess; + import com.rabbitmq.client.Channel; /** @@ -24,9 +26,10 @@ * * @author Mark Pollack * @author Gary Russell + * @author Leonardo Ferreira * @see CachingConnectionFactory */ -public interface ChannelProxy extends Channel { +public interface ChannelProxy extends Channel, RawTargetAccess { /** * Return the target Channel of this proxy. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java index b3ad7c5471..391874f301 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java @@ -292,7 +292,12 @@ public PooledObject makeObject() { @Override public void destroyObject(PooledObject p) throws Exception { - p.getObject().close(); + Channel channel = p.getObject(); + if (channel instanceof ChannelProxy) { + channel = ((ChannelProxy) channel).getTargetChannel(); + } + + ConnectionWrapper.this.physicalClose(channel); } @Override diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactoryTests.java index da3eb6886f..2c94b9be6f 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactoryTests.java @@ -18,9 +18,13 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.commons.pool2.impl.GenericObjectPool; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Queue; @@ -199,6 +203,70 @@ private void createAndCloseConnectionChannelTxAndChannelNonTx( connection.close(); } + @Test + public void evictShouldCloseAllUnneededChannelsWithoutErrors() throws Exception { + PooledChannelConnectionFactory pcf = new PooledChannelConnectionFactory(new ConnectionFactory()); + AtomicReference> channelsReference = new AtomicReference<>(); + AtomicReference> txChannelsReference = new AtomicReference<>(); + AtomicInteger swallowedExceptionsCount = new AtomicInteger(); + pcf.setPoolConfigurer((pool, tx) -> { + if (tx) { + channelsReference.set(pool); + } + else { + txChannelsReference.set(pool); + } + + pool.setEvictionPolicy((ec, u, idleCount) -> idleCount > ec.getMinIdle()); + pool.setSwallowedExceptionListener(ex -> swallowedExceptionsCount.incrementAndGet()); + pool.setNumTestsPerEvictionRun(5); + + pool.setMinIdle(1); + pool.setMaxIdle(5); + }); + + createAndCloseFiveChannelTxAndChannelNonTx(pcf); + + final GenericObjectPool channels = channelsReference.get(); + channels.evict(); + + assertThat(channels.getNumIdle()) + .isEqualTo(1); + assertThat(channels.getDestroyedByEvictorCount()) + .isEqualTo(4); + + final GenericObjectPool txChannels = txChannelsReference.get(); + txChannels.evict(); + assertThat(txChannels.getNumIdle()) + .isEqualTo(1); + assertThat(txChannels.getDestroyedByEvictorCount()) + .isEqualTo(4); + + assertThat(swallowedExceptionsCount.get()) + .isZero(); + } + + private void createAndCloseFiveChannelTxAndChannelNonTx( + org.springframework.amqp.rabbit.connection.ConnectionFactory connectionFactory) { + int channelAmount = 5; + Connection connection = connectionFactory.createConnection(); + + List channels = new ArrayList<>(channelAmount); + List txChannels = new ArrayList<>(channelAmount); + + for (int i = 0; i < channelAmount; i++) { + channels.add(connection.createChannel(false)); + txChannels.add(connection.createChannel(true)); + } + + for (int i = 0; i < channelAmount; i++) { + RabbitUtils.closeChannel(channels.get(i)); + RabbitUtils.closeChannel(txChannels.get(i)); + } + + connection.close(); + } + @Configuration public static class Config { From d5e7c3fb7985066a96411023c748567f0b895d54 Mon Sep 17 00:00:00 2001 From: Leonardo Ferreira Date: Tue, 15 Mar 2022 13:04:41 -0300 Subject: [PATCH 071/737] Add checkConf&Returns into RoutingCF.addTargetCF When only `AbstractRoutingConnectionFactory.addTargetConnectionFactory()` is used, the `AbstractRoutingConnectionFactory.afterPropertiesSet()` throw an `java.lang.IllegalArgumentException: At least one target factory (or default) is required` because the property `confirms` is `null` (the method `addTargetConnectionFactory` doesn't call `checkConfirmsAndReturns` that fill this field). * add `checkConfirmsAndReturns` into the `AbstractRoutingConnectionFactory .addTargetConnectionFactory()` * using `assertThatNoException()` instead of `catch/fail` **Cherry-pick to `2.4.x` & `2.3.x`** --- .../AbstractRoutingConnectionFactory.java | 5 ++++- .../RoutingConnectionFactoryTests.java | 19 ++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.java index 01f12c90aa..5cfa2adc78 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -34,6 +34,7 @@ * @author Artem Bilan * @author Josh Chappelle * @author Gary Russell + * @author Leonardo Ferreira * @since 1.3 */ public abstract class AbstractRoutingConnectionFactory implements ConnectionFactory, RoutingConnectionFactory, @@ -238,6 +239,8 @@ protected void addTargetConnectionFactory(Object key, ConnectionFactory connecti for (ConnectionListener listener : this.connectionListeners) { connectionFactory.addConnectionListener(listener); } + + checkConfirmsAndReturns(connectionFactory); } /** diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/RoutingConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/RoutingConnectionFactoryTests.java index fc5ea11f8a..c22f4b6e7f 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/RoutingConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/RoutingConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -17,6 +17,7 @@ package org.springframework.amqp.rabbit.connection; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.BDDMockito.given; @@ -51,6 +52,7 @@ * @author Artem Bilan * @author Josh Chappelle * @author Gary Russell + * @author Leonardo Ferreira * @since 1.3 */ public class RoutingConnectionFactoryTests { @@ -323,4 +325,19 @@ protected synchronized void redeclareElementsIfNecessary() { assertThat(SimpleResourceHolder.unbind(connectionFactory)).isEqualTo("foo"); } + @Test + void afterPropertiesSetShouldNotThrowAnyExceptionAfterAddTargetConnectionFactory() throws Exception { + AbstractRoutingConnectionFactory routingFactory = new AbstractRoutingConnectionFactory() { + @Override + protected Object determineCurrentLookupKey() { + return null; + } + }; + + routingFactory.addTargetConnectionFactory("1", Mockito.mock(ConnectionFactory.class)); + + assertThatNoException() + .isThrownBy(routingFactory::afterPropertiesSet); + } + } From 0ccfcbe283a2e1bc7a720a6e95a8fa4a447fa0c8 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 15 Mar 2022 13:52:06 -0400 Subject: [PATCH 072/737] GH-1433: Fix DMLC Monitor Thread Name Resolves https://github.com/spring-projects/spring-amqp/issues/1433 Used `beanName` instead of `listenerId` (which falls back to `beanName` if `null`). Containers for annotations are not beans per se. **cherry-pick to 2.4.x, 2.3.x** --- .../AbstractMessageListenerContainer.java | 2 +- .../DirectMessageListenerContainer.java | 2 +- ...rectMessageListenerContainerMockTests.java | 23 +++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index 374f227902..8f0dfe693f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -156,7 +156,7 @@ public abstract class AbstractMessageListenerContainer extends RabbitAccessor private TransactionAttribute transactionAttribute = new DefaultTransactionAttribute(); @Nullable - private String beanName; + private String beanName = "not.a.Spring.bean"; private Executor taskExecutor = new SimpleAsyncTaskExecutor(); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java index 5b92c81ccc..1823d17ddc 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java @@ -391,7 +391,7 @@ private void checkStartState() { protected void doInitialize() { if (this.taskScheduler == null) { ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); - threadPoolTaskScheduler.setThreadNamePrefix(getBeanName() + "-consumerMonitor-"); + threadPoolTaskScheduler.setThreadNamePrefix(getListenerId() + "-consumerMonitor-"); threadPoolTaskScheduler.afterPropertiesSet(); this.taskScheduler = threadPoolTaskScheduler; } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerMockTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerMockTests.java index 6d5dd3b8ec..16580d5184 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerMockTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerMockTests.java @@ -360,6 +360,29 @@ public void testMonitorCancelsAfterTargetChannelChanges() throws Exception { container.stop(); } + @Test + void monitorTaskThreadName() { + DirectMessageListenerContainer container = new DirectMessageListenerContainer(mock(ConnectionFactory.class)); + assertThat(container.getListenerId()).isEqualTo("not.a.Spring.bean"); + container.setBeanName("aBean"); + assertThat(container.getListenerId()).isEqualTo("aBean"); + container.setListenerId("id"); + assertThat(container.getListenerId()).isEqualTo("id"); + container.afterPropertiesSet(); + assertThat(container).extracting("taskScheduler") + .extracting("threadNamePrefix") + .asString() + .startsWith("id-consumerMonitor"); + + container = new DirectMessageListenerContainer(mock(ConnectionFactory.class)); + container.setBeanName("aBean"); + container.afterPropertiesSet(); + assertThat(container).extracting("taskScheduler") + .extracting("threadNamePrefix") + .asString() + .startsWith("aBean-consumerMonitor"); + } + private Envelope envelope(long tag) { return new Envelope(tag, false, "", ""); } From d8aafdafffc4eb3e7380dc4e20f78545b6324c38 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 15 Dec 2021 15:22:20 -0500 Subject: [PATCH 073/737] Remove usage of obsolete NestedIOException Related to https://github.com/spring-projects/spring-framework/issues/28198 Replaces its usage with the standard `IOException` which has supported a root cause since Java 6. --- .../converter/SerializerMessageConverter.java | 10 +++++---- .../amqp/utils/SerializationUtils.java | 22 +++++++++---------- .../SerializerMessageConverterTests.java | 18 ++++++++------- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SerializerMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SerializerMessageConverter.java index 5b12f288b6..bdbe2bde0d 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SerializerMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SerializerMessageConverter.java @@ -22,16 +22,17 @@ import java.io.ObjectInputStream; import java.io.ObjectStreamClass; import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; import org.springframework.beans.DirectFieldAccessor; import org.springframework.core.ConfigurableObjectInputStream; -import org.springframework.core.NestedIOException; import org.springframework.core.serializer.DefaultDeserializer; import org.springframework.core.serializer.DefaultSerializer; import org.springframework.core.serializer.Deserializer; import org.springframework.core.serializer.Serializer; +import org.springframework.lang.Nullable; /** * Implementation of {@link MessageConverter} that can work with Strings or native objects @@ -47,10 +48,11 @@ * * @author Dave Syer * @author Gary Russell + * @author Artem Bilan */ public class SerializerMessageConverter extends AllowedListDeserializingMessageConverter { - public static final String DEFAULT_CHARSET = "UTF-8"; + public static final String DEFAULT_CHARSET = StandardCharsets.UTF_8.name(); private volatile String defaultCharset = DEFAULT_CHARSET; @@ -80,7 +82,7 @@ public void setIgnoreContentType(boolean ignoreContentType) { * * @param defaultCharset The default charset. */ - public void setDefaultCharset(String defaultCharset) { + public void setDefaultCharset(@Nullable String defaultCharset) { this.defaultCharset = (defaultCharset != null) ? defaultCharset : DEFAULT_CHARSET; } @@ -182,7 +184,7 @@ protected Class resolveClass(ObjectStreamClass classDesc) return objectInputStream.readObject(); } catch (ClassNotFoundException ex) { - throw new NestedIOException("Failed to deserialize object type", ex); + throw new IOException("Failed to deserialize object type", ex); } } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/utils/SerializationUtils.java b/spring-amqp/src/main/java/org/springframework/amqp/utils/SerializationUtils.java index ccdb9ce6b3..d7e69f455e 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/utils/SerializationUtils.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/utils/SerializationUtils.java @@ -26,7 +26,6 @@ import java.util.Set; import org.springframework.core.ConfigurableObjectInputStream; -import org.springframework.core.NestedIOException; import org.springframework.util.ObjectUtils; import org.springframework.util.PatternMatchUtils; @@ -35,6 +34,7 @@ * * @author Dave Syer * @author Gary Russell + * @author Artem Bilan */ public final class SerializationUtils { @@ -111,22 +111,22 @@ public static Object deserialize(InputStream inputStream, Set allowedLis throws IOException { try ( - ObjectInputStream objectInputStream = new ConfigurableObjectInputStream(inputStream, classLoader) { + ObjectInputStream objectInputStream = new ConfigurableObjectInputStream(inputStream, classLoader) { - @Override - protected Class resolveClass(ObjectStreamClass classDesc) - throws IOException, ClassNotFoundException { - Class clazz = super.resolveClass(classDesc); - checkAllowedList(clazz, allowedListPatterns); - return clazz; - } + @Override + protected Class resolveClass(ObjectStreamClass classDesc) + throws IOException, ClassNotFoundException { + Class clazz = super.resolveClass(classDesc); + checkAllowedList(clazz, allowedListPatterns); + return clazz; + } - }) { + }) { return objectInputStream.readObject(); } catch (ClassNotFoundException ex) { - throw new NestedIOException("Failed to deserialize object type", ex); + throw new IOException("Failed to deserialize object type", ex); } } diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/SerializerMessageConverterTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/SerializerMessageConverterTests.java index 74eb71cf96..744adb5748 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/SerializerMessageConverterTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/SerializerMessageConverterTests.java @@ -23,9 +23,11 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -33,23 +35,23 @@ import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.utils.test.TestUtils; -import org.springframework.core.NestedIOException; import org.springframework.core.serializer.DefaultDeserializer; import org.springframework.core.serializer.Deserializer; /** * @author Mark Fisher * @author Gary Russell + * @author Artem Bilan */ public class SerializerMessageConverterTests extends AllowedListDeserializingMessageConverterTests { @Test - public void bytesAsDefaultMessageBodyType() throws Exception { + public void bytesAsDefaultMessageBodyType() { SerializerMessageConverter converter = new SerializerMessageConverter(); Message message = new Message("test".getBytes(), new MessageProperties()); Object result = converter.fromMessage(message); assertThat(result.getClass()).isEqualTo(byte[].class); - assertThat(new String((byte[]) result, "UTF-8")).isEqualTo("test"); + assertThat(new String((byte[]) result, StandardCharsets.UTF_8)).isEqualTo("test"); } @Test @@ -65,7 +67,7 @@ public void messageToString() { @Test public void messageToBytes() { SerializerMessageConverter converter = new SerializerMessageConverter(); - Message message = new Message(new byte[] { 1, 2, 3 }, new MessageProperties()); + Message message = new Message(new byte[]{ 1, 2, 3 }, new MessageProperties()); message.getMessageProperties().setContentType(MessageProperties.CONTENT_TYPE_BYTES); Object result = converter.fromMessage(message); assertThat(result.getClass()).isEqualTo(byte[].class); @@ -126,7 +128,7 @@ public void stringToMessage() throws Exception { @Test public void bytesToMessage() throws Exception { SerializerMessageConverter converter = new SerializerMessageConverter(); - Message message = converter.toMessage(new byte[] { 1, 2, 3 }, new MessageProperties()); + Message message = converter.toMessage(new byte[]{ 1, 2, 3 }, new MessageProperties()); String contentType = message.getMessageProperties().getContentType(); byte[] body = message.getBody(); assertThat(contentType).isEqualTo("application/octet-stream"); @@ -168,7 +170,7 @@ public void testDefaultDeserializerClassLoader() throws Exception { } @Test - public void messageConversionExceptionForClassNotFound() throws Exception { + public void messageConversionExceptionForClassNotFound() { SerializerMessageConverter converter = new SerializerMessageConverter(); TestBean testBean = new TestBean("foo"); Message message = converter.toMessage(testBean, new MessageProperties()); @@ -177,8 +179,8 @@ public void messageConversionExceptionForClassNotFound() throws Exception { byte[] body = message.getBody(); body[10] = 'z'; assertThatThrownBy(() -> converter.fromMessage(message)) - .isExactlyInstanceOf(MessageConversionException.class) - .hasCauseExactlyInstanceOf(NestedIOException.class); + .isExactlyInstanceOf(MessageConversionException.class) + .hasCauseExactlyInstanceOf(IOException.class); } } From e4ea880e7ba3d19d0fb389f823fa8ccf2ed7defb Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 21 Mar 2022 12:53:27 -0400 Subject: [PATCH 074/737] Upgrade versions; prepare for release --- build.gradle | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index 5b6a11023f..d1ca227d5a 100644 --- a/build.gradle +++ b/build.gradle @@ -47,24 +47,24 @@ ext { commonsPoolVersion = '2.11.1' googleJsr305Version = '3.0.2' hamcrestVersion = '2.2' - hibernateValidationVersion = '6.2.0.Final' - jacksonBomVersion = '2.13.1' + hibernateValidationVersion = '6.2.3.Final' + jacksonBomVersion = '2.13.2' jaywayJsonPathVersion = '2.6.0' junit4Version = '4.13.2' junitJupiterVersion = '5.8.2' log4jVersion = '2.17.1' logbackVersion = '1.2.3' lz4Version = '1.8.0' - micrometerVersion = '2.0.0-SNAPSHOT' - mockitoVersion = '3.11.2' + micrometerVersion = '2.0.0-M3' + mockitoVersion = '4.0.0' rabbitmqStreamVersion = '0.4.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.13.1' rabbitmqHttpClientVersion = '3.12.1' - reactorVersion = '2020.0.15' + reactorVersion = '2020.0.17' snappyVersion = '1.1.8.4' - springDataCommonsVersion = '3.0.0-M1' - springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.0-M2' - springRetryVersion = '1.3.1' + springDataCommonsVersion = '3.0.0-M2' + springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.0-M3' + springRetryVersion = '1.3.2' zstdJniVersion = '1.5.0-2' } From 65edbde6cb5091979d3a8ef5b9f7441b17d00047 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 21 Mar 2022 12:56:11 -0400 Subject: [PATCH 075/737] Upgrade Gradle --- gradle/wrapper/gradle-wrapper.jar | Bin 59536 -> 59821 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180f2ae8848c63b8b4dea2cb829da983f2fa..41d9927a4d4fb3f96a785543079b8df6723c946b 100644 GIT binary patch delta 8958 zcmY+KWl$VIlZIh&f(Hri?gR<$?iyT!TL`X;1^2~W7YVSq1qtqM!JWlDxLm%}UESUM zndj}Uny%^UnjhVhFb!8V3s(a#fIy>`VW15{5nuy;_V&a5O#0S&!a4dSkUMz_VHu3S zGA@p9Q$T|Sj}tYGWdjH;Mpp8m&yu&YURcrt{K;R|kM~(*{v%QwrBJIUF+K1kX5ZmF zty3i{d`y0;DgE+de>vN@yYqFPe1Ud{!&G*Q?iUc^V=|H%4~2|N zW+DM)W!`b&V2mQ0Y4u_)uB=P@-2`v|Wm{>CxER1P^ z>c}ZPZ)xxdOCDu59{X^~2id7+6l6x)U}C4Em?H~F`uOxS1?}xMxTV|5@}PlN%Cg$( zwY6c}r60=z5ZA1L zTMe;84rLtYvcm?M(H~ZqU;6F7Evo{P7!LGcdwO|qf1w+)MsnvK5^c@Uzj<{ zUoej1>95tuSvDJ|5K6k%&UF*uE6kBn47QJw^yE&#G;u^Z9oYWrK(+oL97hBsUMc_^ z;-lmxebwlB`Er_kXp2$`&o+rPJAN<`WX3ws2K{q@qUp}XTfV{t%KrsZ5vM!Q#4{V& zq>iO$MCiLq#%wXj%`W$_%FRg_WR*quv65TdHhdpV&jlq<=K^K`&!Kl5mA6p4n~p3u zWE{20^hYpn1M}}VmSHBXl1*-)2MP=0_k)EPr#>EoZukiXFDz?Di1I>2@Z^P$pvaF+ zN+qUy63jek2m59;YG)`r^F3-O)0RDIXPhf)XOOdkmu`3SMMSW(g+`Ajt{=h1dt~ks ztrhhP|L4G%5x79N#kwAHh5N){@{fzE7n&%dnisCm65Za<8r_hKvfx4Bg*`%-*-Mvn zFvn~)VP@}1sAyD+B{{8l{EjD10Av&Mz9^Xff*t`lU=q=S#(|>ls520;n3<}X#pyh& z*{CJf7$*&~!9jMnw_D~ikUKJ2+UnXmN6qak{xx%W;BKuXt7@ky!LPI1qk?gDwG@@o zkY+BkIie>{{q==5)kXw(*t#I?__Kwi>`=+s?Gq6X+vtSsaAO&Tf+Bl$vKnzc&%BHM z=loWOQq~n}>l=EL(5&6((ESsQC3^@4jlO5Od{qN#sWV)vqXw}aA>*uvwZopNN(|-T zRTF%5Y_k1R$;(d-)n;hWex{;7b6KgdAVE@&0pd(*qDzBO#YZV%kh%pYt1`hnQ(Fa& zYiDrOTDqk5M7hzp9kI2h!PxNnuJ&xl*zF8sx6!67bA49R1bmUF5bpK&&{eI0U~cH}PM z3aW1$lRb|ItkG5~_eBNu$|I|vYIdAA9a!pVq<+UTx*M}fG`23zxXp&E=FfnY- zEzKj;Cu_s4v>leO7M2-mE(UzKHL4c$c`3dS*19OpLV^4NI*hWWnJQ9lvzP4c;c?do zqrcsKT*i~eIHl0D3r4N{)+RsB6XhrC^;sp2cf_Eq#6*CV;t8v=V!ISe>>9kPgh}NI z=1UZutslxcT$Ad;_P^;Oouoa(cs!Ctpvi>%aQ+Zp=1d|h{W9Wmf7JWxa(~<#tSZ?C%wu4_5F!fc!<@PIBeJ)Nr^$bB6!_Gic_7}c3J{QI~Gg5g5jTp9}V6KYgrgaX>pJt}7$!wOht&KO|+z{Iw@YL|@~D zMww}+lG}rm2^peNx>58ME||ZQxFQeVSX8iogHLq_vXb`>RnoEKaTWBF-$JD#Q4BMv zt2(2Qb*x-?ur1Y(NsW8AdtX0#rDB?O(Vs4_xA(u-o!-tBG03OI!pQD+2UytbL5>lG z*(F)KacHqMa4?dxa(Vcrw>IIAeB$3cx#;;5r2X;HE8|}eYdAgCw#tpXNy7C3w1q`9 zGxZ6;@1G%8shz9e+!K2MO*{_RjO}Jo6eL3{TSZ>nY7)Qs`Dhi5><@oh0r)gT7H-?3 zLDsd^@m%JvrS8sta5`QiZNs^*GT}Hiy^zjK2^Ni%`Z|ma)D2 zuyumbvw$M8$haCTI~6M%d4+P)uX%u{Sfg4Al+F7c6;O-*)DKI7E8izSOKB#FcV{M+ zEvY0FBkq!$J0EW$Cxl}3{JwV^ki-T?q6C30Y5e&p@8Rd?$ST-Ghn*-`tB{k54W<>F z5I)TFpUC!E9298=sk>m#FI4sUDy_!8?51FqqW!9LN1(zuDnB3$!pEUjL>N>RNgAG~-9Xm|1lqHseW(%v&6K(DZ3Pano(1-Qe?3%J&>0`~w^Q-p&@ zg@HjvhJk?*hpF7$9P|gkzz`zBz_5Z!C4_-%fCcAgiSilzFQef!@amHDrW!YZS@?7C zs2Y9~>yqO+rkih?kXztzvnB^6W=f52*iyuZPv$c42$WK7>PHb z6%MYIr5D32KPdwL1hJf{_#jn?`k(taW?mwmZVvrr=y~fNcV$`}v(8};o9AjOJumS4 z`889O91^pkF+|@$d9wVoZ3;^j;^sUs&Ubo_qD&MTL%O z&*SE0ujG~zm;?x)8TLC&ft))nyI zcg44@*Q{cYT+qGrA=In_X{NNCD+B0w#;@g)jvBU;_8od6U>;7HIo@F*=g8CQUo(u^ z3r4FJ7#<@)MXO&5+DgKE&^>^`r!loe7CWE*1k0*0wLFzSOV8jvlX~WOQ?$1v zk$Or}!;ix0g78^6W;+<=J>z@CBs!<<)HvF(Ls-&`matpesJ5kkjC)6nGB@b{ii6-Uoho$BT%iJgugTOeZ$5Xo4D7Pd< zC*LJh5V@2#5%aBZCgzlQi3@<_!VfiL07ywc)ZbwKPfcR|ElQoS(8x|a7#IR}7#Io= zwg4$8S{egr-NffD)Fg&X9bJSoM25pF&%hf>(T&9bI}=#dPQyNYz;ZZ7EZ=u1n701SWKkZ9n(-qU ztN`sdWL1uxQ1mKS@x11;O|@^AD9!NeoPx}?EKIr!2>1Qq4gjfGU)tr6?Z5l7JAS3j zZeq{vG{rb%DFE4%$szK}d2UzB{4>L?Tv+NAlE*&Nq6g+XauaSI+N2Y8PJLw+aNg1p zbxr|hI8wcMP&&+(Cu|%+Jq|r>+BHk@{AvfBXKiVldN)@}TBS0LdIpnANCVE26WL-} zV}HJ^?m&$Rkq;Zf*i-hoasnpJVyTH__dbGWrB_R55d*>pTyl6(?$EO@>RCmTX1Hzr zT2)rOng?D4FfZ_C49hjMV*UonG2DlG$^+k=Y%|?Dqae4}JOU=8=fgY4Uh!pa9eEqf zFX&WLPu!jArN*^(>|H>dj~g`ONZhaaD%h_HHrHkk%d~TR_RrX{&eM#P@3x=S^%_6h zh=A)A{id16$zEFq@-D7La;kTuE!oopx^9{uA3y<}9 z^bQ@U<&pJV6kq7LRF47&!UAvgkBx=)KS_X!NY28^gQr27P=gKh0+E>$aCx&^vj2uc}ycsfSEP zedhTgUwPx%?;+dESs!g1z}5q9EC+fol}tAH9#fhZQ?q1GjyIaR@}lGCSpM-014T~l zEwriqt~ftwz=@2tn$xP&-rJt?nn5sy8sJ5Roy;pavj@O+tm}d_qmAlvhG(&k>(arz z;e|SiTr+0<&6(-An0*4{7akwUk~Yf4M!!YKj^swp9WOa%al`%R>V7mi z+5+UodFAaPdi4(8_FO&O!Ymb#@yxkuVMrog(7gkj$G@FLA#ENMxG)4f<}S%Fn?Up$+C%{02AgMKa^ z4SFGWp6U>{Q6VRJV}yjxXT*e`1XaX}(dW1F&RNhpTzvCtzuu;LMhMfJ2LBEy?{^GHG!OF!! zDvs64TG)?MX&9NCE#H3(M0K>O>`ca0WT2YR>PTe&tn?~0FV!MRtdb@v?MAUG&Ef7v zW%7>H(;Mm)RJkt18GXv!&np z?RUxOrCfs;m{fBz5MVlq59idhov21di5>WXWD-594L-X5;|@kyWi@N+(jLuh=o+5l zGGTi~)nflP_G}Yg5Pi%pl88U4+^*ihDoMP&zA*^xJE_X*Ah!jODrijCqQ^{=&hD7& z^)qv3;cu?olaT3pc{)Kcy9jA2E8I)#Kn8qO>70SQ5P8YSCN=_+_&)qg)OYBg|-k^d3*@jRAeB?;yd-O1A0wJ z?K*RDm|wE<(PBz~+C%2CTtzCTUohxP2*1kE8Of~{KRAvMrO_}NN&@P7SUO{;zx0iK z@or9R8ydYOFZf(cHASCAatL%;62IL27~SmASr(7F&NMr+#gNw@z1VM z_ALFwo3)SoANEwRerBdRV`>y`t72#aF2ConmWQp(Xy|msN9$yxhZ1jAQ67lq{vbC5 zujj|MlGo`6Bfn0TfKgi(k=gq0`K~W+X(@GzYlPI4g0M;owH3yG14rhK>lG8lS{`!K z+Nc@glT-DGz?Ym?v#Hq|_mEdPAlHH5jZuh*6glq!+>Lk$S%ED2@+ea6CE@&1-9a?s znglt|fmIK}fg<9@XgHe4*q!aO<-;Xj$T?IzB-{&2`#eA6rdtCi80mpP&vw(Uytxu$#YzNI_cB>LS zmim>ys;ir;*Dzbr22ZDxO2s;671&J0U<9(n1yj)J zHFNz=ufPcQVEG+ePjB<5C;=H0{>Mi*xD>hQq8`Vi7TjJ$V04$`h3EZGL|}a07oQdR z?{cR(z+d>arn^AUug&voOzzi$ZqaS)blz-z3zr;10x;oP2)|Cyb^WtN2*wNn`YX!Y z+$Pji<7|!XyMCEw4so}xXLU)p)BA~2fl>y2Tt}o9*BPm?AXA8UE8a;>rOgyCwZBFa zyl42y`bc3}+hiZL_|L_LY29vVerM+BVE@YxK>TGm@dHi@Uw*7AIq?QA9?THL603J% zIBJ4y3n8OFzsOI;NH%DZ!MDwMl<#$)d9eVVeqVl(5ZX$PPbt*p_(_9VSXhaUPa9Qu z7)q4vqYKX7ieVSjOmVEbLj4VYtnDpe*0Y&+>0dS^bJ<8s*eHq3tjRAw^+Mu4W^-E= z4;&namG4G;3pVDyPkUw#0kWEO1;HI6M51(1<0|*pa(I!sj}F^)avrE`ShVMKBz}nE zzKgOPMSEp6M>hJzyTHHcjV%W*;Tdb}1xJjCP#=iQuBk_Eho6yCRVp&e!}4IBJ&?ksVc&u#g3+G$oNlJ?mWfADjeBS-Ph3`DKk-~Z70XugH8sq2eba@4 zIC1H_J$`9b$K`J)sGX3d!&>OmC@@rx1TL~NinQOYy72Q_+^&Mg>Ku(fTgaXdr$p_V z#gav1o{k~c>#)u3r@~6v^o)Lf=C{rAlL@!s457pq)pO;Cojx7U{urO4cvXP|E>+dV zmr2?!-5)tk-&*ap^D^2x7NG6nOop2zNFQ9v8-EZ{WCz-h36C)<^|f{V#R_WE^@(T0+d-at5hXX{U?zak*ac-XnyINo+yBD~~3O1I=a z99|CI>502&s-Qi5bv>^2#cQ%ut<4d7KgQ^kE|=%6#VlGiY8$rdJUH{sra;P~cyb_i zeX(kS%w0C?mjhJl9TZp8RS;N~y3(EXEz13oPhOSE4WaTljGkVXWd~|#)vsG6_76I)Kb z8ro?;{j^lxNsaxE-cfP;g(e;mhh3)&ba}li?woV2#7ByioiD>s%L_D;?#;C#z;a(N z-_WY<=SH42m9bFQ>Nb z@4K$@4l8pD7AKxCR>t0%`Qoy9=hA?<<^Vcj8;-E+oBe3ReW1`el8np8E$k{LgFQ}2 z2t8a`wOXFdJ9!5$&mEfD1CnJ)TB+RJih88-Zos9@HZ# zL#{qfbF0ARTXkR@G{lwlOH~nnL)1jcyu!qv2`57S&%oKz0}r{~l9U_UHaJ5!8#nrs z?2FrL`mxnzu&{bweD&62)ilz*?pYIvt`T!XFVVA78})p1YEy7 z8fK#s?b~Yo$n7&_a?EBdXH-_W)Z44?!;DFx6pZ?~RArtBI*Qm4~6nX6Z_T*i$bQPE;Qz?DAPstpGSqr-AJ zo%m9cA`oDDm?&dTaoh_>@F>a?!y4qt_;NGN9Z<%SS;fX-cSu|>+Pba22`CRb#|HZa z;{)yHE>M-pc1C0mrnT~80!u&dvVTYFV8xTQ#g;6{c<9d!FDqU%TK5T6h*w*p980D~ zUyCb`y3{-?(mJFP)0*-Nt;mI$-gc4VQumh|rs&j_^R{sgTPF`1Xja2YWstsKFuQ(d zmZMxV$p$|qQUXchu&8%J(9|)B?`~rIx&)LqDS>ob5%gTeTP#Sbny#y*rnJ&?(l=!( zoV~}LJ1DPLnF8oyM(2ScrQ0{Q4m4-BWnS4wilgCW-~~;}pw=&<+HggRD_3c@3RQIr z9+-%!%}u_{`YS=&>h%kPO3ce}>y!d-zqiniNR-b5r97u;+K6HA2tS>Z#cV{+eFI`* zd8RMGAUtX1KWfPV;q<-5JAykS+2sY$2~UX+4461a(%{P#{rwFPu0xpIuYlbgD{C7C z=U{FUarVTYX6ZUq3wE@G^QT4H2Re;n$Fz9cJ>hABl)9T8pozqbA1)H-%1=WKm^QMu zjnUZ&Pu>q+X&6Co*y#@pxc-4waKMInEPGmE_>3@Ym3S*dedSradmc5mlJn`i0vMW6 zhBnGQD^Z;&S0lnS0curqDO@({J7kTtRE+Ra?nl^HP9<)W&C>~`!258f$XDbyQOQXG zP8hhySnarOpgu8xv8@WlXnm(Uk~)_3$Sg0vTbU3 z{W!5B(L3{Yy3K5PN<@jEarAtja`}@KYva&zFRF*s+_%jIXh$T(S=an8?=Ry3H*NRqWgsM`&!#|@kf1>=4q%bFw7^Rhz!z5I zyI^zU8_R1WN9`88Z=n>pIZQ`Ixr~_9G%Q}@A7rd#*%y7G zXl^Id=^ZL?Rx}}gWXCqzj9C6;x(~mAH|$JteXa1MH<6UQig@!Hf~t}B%tP0I|H&;y zO6N0}svOa1a^PyP9N5?4W6VF%=Bj{qHUgc8@siw4bafT=UPFSoQqKgyUX>sXTBZ=x zOh^Ad!{kOM9v{%5y}`-8u*T&C7Vq6mD%GR}UeU(*epO&qgC-CkD;%=l)ZuinSzHM` z{@`j&_vC6dDe{Yb9k@1zeV_K6!l(@=6ucoI=R^cH=6{i71%4W3$J-?<8Qn#$-DMtA z6Qqi)t?4ifrt%3jSA#6ji#{f(($KBL-iQh-xrC||3U3lq`9>r)>X%oLvtimuHW-)} zy}>9~|M>w4eES`g7;iBM%Se5-OP%1U6gNWp3AZqT8C6OlFFfQ$|7LL;tBV)(qlp4K zruar^K8FnJN3@_}B;G`a~H`t|3+6d>q3#`ctTkE-D^1#d9NalQ04lH*qUW2!V zhk7#z8OwHhSl8w14;KctfO8ubZJ4$dEdpXE78wABz=n5*=q9ex3S}`e7x~~V-jmHOhtX2*n+pBslo3uosdE7xABK=V#-t{1Hd~?i z{i~%Bw6NYF+F$aK$M`r#xe=NxhA5=p%i7!$);sd>Q}#`G?Q~fygrMXmZw?0#5#17W}6Tj+&kFexG{!mYl5FoA99}3G9l;3lVQ^ z48^~gsVppE*x91WheqI(A%F0Z#$#1UJP1R12Mj9r)y(A?a+iquX+d8WD4WAQJ_!oq z9rTISr7bPd(GTP57xm$}C}&kjMivi;zi^Y9g3&X0A;ovdJ?{%_wHgt%%9P&N4H z^XzV(uNA4 zAP`hgP6BEN5`YXh|DF~6Pud?~gWfhUKoPX4>z|}0aocC&K+AoV%|SX*N!wGq3|y< zg4lP(04XIPmt6}$N!dTk+pZv>u;MTB{L4hp9uXk7>aS!6jqM2lVr%{)H3$O127TSZ z0x9hi0k-P?nWFdQ0K`pykqUIT&jD~B0tHP{ffS(}fZ(aW$oBWTSfHO!A^><6vA?qar%tzN-5NQO zL&|F{nGiQyzNJ+bM$Y`n=Lx^3wTG^o2bGB@cwr1eb+6c-1tN=U+Db;bc~eJ!hwM{SbI=#g?$!PjDB+) zPgU_2EIxocr*EOJG52-~!gml&|D|C2OQ3Y(zAhL}iae4-Ut0F*!z!VEdfw8#`LAi# zhJ_EM*~;S|FMV6y%-SduHjPOI3cFM(GpH|HES<}*=vqY+64%dJYc|k?n6Br7)D#~# zEqO(xepfaf2F{>{E2`xb=AO%A<7RtUq6kU_Iu0m?@0K(+<}u3gVw5fy=Y4CC*{IE3 zLP3YBJ7x+U(os5=&NT%gKi23bbaZ`@;%ln)wp4GpDUT$J8NtFDHJzIe_-t}{!HAsh zJ4<^WovY};)9IKAskSebdQiXv$y5}THuJZ}ouoElIZRui=6lrupV|_Jz=9^&;@HwL;J#@23k?A;k`0Bgf;ioO>W`IQ+4? z7A)eKoY4%+g%=w;=Vm8}H>@U*=*AWNtPqgWRqib#5RTGA@Q=43FrQn3J`GkTUV5yp0U`EOTqjfp+-9;0F8!dMEwwcK%(6`8sDD^aR04 zd6O5vh|Xk?&3dy4f|1QK&Ulf{h6Iq;d-&*ti#Ck>wZFG;GHwc?b;X~eBITx49>2d8 z4HcK&1&DvEGT6kXdzAm4oO8%c}8OBt~8H956_;YP-ss*uMf==a+%w~F>Qkm7r)IAuxuoX}h92$gHqbFUun#8m zWHdy`Zrm#=Pa98x8cO0vd@Tgkr*lm0{dky+Gocr0P8y%HGEI#c3qLqIRc`Oq_C%*; zG+QTr(#Q|yHKv6R@!DmLlwJQ3FAB)Yor-I4zyDyqM4yp5n2TrQH>gRt*Zw0+WI-Sj`EgmYHh=t9! zF6lz^xpqGGpo6!5`sc0a^FVhy_Uxq|@~(1@IIzV)nTpY9sY`CV!?8e&bB8=M&sYEb z2i}fvKdhp9Hs68Y-!QJ<=wE(iQ5+49tqt;Rh|jhYrI5VW-mIz|UY{h8E=rC5sh#DU z?wGgk-Tn!I?+Zer7pHlF_Z^!Kd1qkS3&lv#%s6-<5Y%jQL${cge5=G5Ab?D&|9$Y~ zf%rJC2+=2vg;y0-SJb3<@3%}BO$T$C66q$L_H33a`VUbgW~N(4B=v5(<=My|#|J7q z*Ox4wL4kbJd_~EjLTABSu4U7Jk#`y(6O*U6(k6XxM}CtGZB(H@3~kh*zaGRXM}Iwp zQ%xFk2>@wiZrVCV_G4G~v;NebCQ%T7{SDyPpSv&dT@Cn)Mx@IK*IdNrj{*4pkV4wv z)y0J538h>cpB7iPSzA~x24T`{dzNkpvGIqvt1Dvdq@o-`B=$hkczX8$yFMhsWNK-X zxr$kR$tMD0@W)Vxe1^t9qVmsg&K^F@u84)(n2dttIEAZFN6VD$&tskpG%SI7whGL3 z)DeRiwe&?8m7U{G`oW8!SCi*dM>oYL%UKQnKxV_0RXAEBQg1kStExGEUVwLJ0orGGwb7uv+kPDl7_E2*iD|J*=8A@;XCvwq0aw5oJYN*Yh&o=l} z2z8YKb-fIAH5spql4eXqp*)o2*b>#1@DSt?zZi{GPj0gH&Nm+EI<3^z0w%YTEV4xw zI6$+=Faa|Y4o5i0zm5lOg|&tmnJ806DBovU@Ll6XsA;NRrTK~t*AAJIAS=v-UZ%Pr z$oddI@NRir&erzCwq|)ciJemr-E061j{0Vc@Ys7K(mW|JYj*$+i1Q8XlIK8T?TYS(AXu$`2U zQ@fHxc=AVHl_}cRZQ)w0anMEoqRKKIvS^`<-aMf*FM`NsG&Uowneo+Ji$7DUDYc7*Hjg;-&aHM%3 zXO6cz$$G};Uqh+iY7Wpme>PHG4cu(q;xyskNLs$^uRRMfEg?8Cj~aE-ajM%CXkx0F z>C?g3tIA#9sBQOpe`J+04{q7^TqhFk^F1jFtk4JDRO*`d-fx`GYHb=&(JiaM1b?Y^ zO3Kj3sj76ieol|N$;>j@t#tKj=@*gP+mv}KwlTcPYgR$+)2(gk)2JNE=jSauPq!$< z<|?Sb%W)wS)b>b6i{8!x!^!xIdU3{CJFVnTcw0j{M%DUCF=_>eYYEUWnA-|B(+KYL z_W_`JI&&u^@t0})@DH^1LDuT0s3dMpCHIbYBgOT4Zh_4yHbSqRbtIKndeT4Q*Jg91 z@>rO!^t-G~*AIW;FQ$3J=b;oGg8?CTa~qNCb>&cgp@e;?0AqA&paz~(%PYO+QBo4( zp?}ZdSMWx0iJm7HVNk9A#^9Osa#GPJ!_pYEW}($8>&2}fbr@&ygZ?${A7_9?X$(&5 z#~-hxdPQwCNEpf=^+WH-3`2LxrrBMTa}~qJC9S;VzhG!On^JLyW6WkF{8aAE$sM+( zxr8xLW(KIjI`Rm(24r3OJBk<3GF=G!uSP0-G&AY32mLm8q=#Xom&Pqv=1C{d3>1^ zAjsmV@XZ%BKq^eUfBpa8KvO8ob|F3hAjJv*yo2Bhl0)KUus{qA9m8jf)KnOGGTa6~4>3@J_VzkL|vYPl*uL+Ot*Q7W!f5rJw5+AsjP_IfL+-S*2p| zB7!FhjvkUTxQkGWGSg{X;h~dK>gAJivW?88Nu!3o>ySDaABn$rAYt086#27fbjPQS zhq>55ASvm*60qRdVOY9=bU^+{Pi#!OaZwENN;zy5?EztOHK-Q5;rCuiFl}BSc1YaQ zC-S{=KsGDz@Ji9O5W;XxE0xI|@3o6(2~i4b8Ii9VT;^G$*dRw(V?=br)D&q^XkeBX z+gl~+R@rVD-Hwv@7RHV?Bip5KMI)aV^&snt?H<$Nt=OPx#VxF&BGi?2A2+lNOYywNUGMeGL;|(=UjGDtLG0sN&LpGx;|U;xa13s z;W_|SPk^G}!M9_^pO zA3bt3-tca%^42sHeDtfcC0S3w3H1ny!Bxpa=*k?XRPpx9Bb-gx1J9Yvx)4J(8cG+q z(iCPZ9dsf3#QVyZgD_MW#G#qgV)olu$59&3(PzQfw@%4uZ~<5J=ABvdY43(Qnp{;G zHg3>@T#>DbTuhFl3)fb3TFqdh)V2aq7!;&JOHseTWukvA7}(iGUq;v-{2J0iHSNHq z;+)h!p6Ok^+Sp8-jgL($n6Qu47xyE`cFO5SdZR6;R!FET`tm#0D37z339Suxjpv+s z*=%2-N$N?X&0?x_uut3erF@aBGj;9$k9?3FlbDO{RQa1_qtxrh4!4#fjp4x~akvdTp@ zos?^Q&XE;3N93s4rHQGPrV7+au1$$aB6$hLy*Yz_kN$~dweb9PcB!eYVQTGjFuJP> zZCEwBtb>TIgIO^qAzq@Bv-qud_ZD-2W<_at&ml-gv`tPt$@DF5`HlA zM>DmmMkpv&Zm-8)Y#0bLQf4MpD4_-7M8eu6rh(tL8dq8onHs#R9J~dGd2IaXXMC~h z91pKhnQa%Fsn29nAA1;x(%oC zhca~qQDJaMf?wFrl-Pj;e$bZMYmMF!Y3Lv&Sb?Sjn#!NVx&NDyc^$b4uYyo2OmERa zRz;yDGd@JTykzFLe|Wk-y7#3x`6$wt$zR8r48mdUvfbeL+4D|Z``~7$PrE@qc7rZe zVsIoIbCwzjLZ@_M1*bD{HaYn();Z1-q*-I{tEnTZ(}Zmk&%MXSNBX>o| z-u*RNkAyKC-Srp7c-=@5f)xMWg>o2WWl}j6j9=8+D8;T z>0*0q#;qw8%U8i;6s0fu#I*%(g*@@a2Er@@nyI}{=@W{Z-;`=wN4N~>6Xrh&z#g}l zN1g5}0-#(nHUTv_rl2{yUZ;h#t&Fd?tY!7L%ClY)>uH-Ny2ET$lW$S)IQiN79H)D^ zb&0AXYkupy0~w8)*>Sj_p9}4L?lGTq%VG|2p`nWGhnM^!g|j-|O{%9Q%swOq63|*W zw$(N_laI}`ilB+o!a-wl?er~;;3+)$_akSQ!8YO_&-e*SI7n^(QQ;X0ZE`{4f!gAl z5$d+9CKVNonM!NO_frREICIAxOv)wm>}-k?iRisM`R7;=lyo|E_YR~FpS&PS`Lg0f zl-ON<0S%Uix8J%#yZdkCz4YNhcec<|7*P(JsM#>-L>+tYg_71q9~70FAc^6KW5jql zw!crdgVLH1G_eET=|SEc977;)ezVC|{PJZfra|}@rD;0s&@61mTEBJtILllg{%{vN zfhb&lq0yChaLhnJ-Qb62MB7`>M;|_ceHKZAeeh@#8tbrK!ArP6oXIhMK;dhEJTY`@ z0Tq>MIe0`7tGv)N*F0IGYSJv0vN?Az8g+4K9S!pW2~9F4W(_U_T=jCZrzuZ3*|__T zONp_UWmyePv8C~rckc?Xji;Z5OEqg zC*Um)i;Wh4TEwqReQdVVbUKT^2>Tpi6z_^-uF*adUFug4i@JhzpWT^Sk&E>CyP2?H zWf6x}ehuTs6wvzCnTU&gYzT029Nz19(In1WC z`(1IGmi!O%2AR|BjQa4Q0~u)kM%}?xQyjWuQ16^Gp++;`vr7!k--UZWM*~7Zl|ceO@I3`OpaRhD;YoCuo5IC0uHx>9 z478hu@H|e0Zlo)Zj@01#;8BDs@991xe~^9uG2}UXLM(m7fa}AMwX*tjioBeV&Q8Gx zSq$6wZFkRBK`cMI>R(@W@+lo2t)L+4q-negWRLWZBz*|%=W4v62JrmzNuOtA*x)QE z5L%=OH#@KMdB%Jp^r?0tE}5-*6oP`-lO7Sf)0)n*e<{HA=&qhLR)oD8-+V}Z4=md) z+k9lKf64DB2hAT)UaCP~di?-V3~JBH7itYyk~L6hrnxM%?RKntqd`=!b|e7eFnAcu z3*V;g{xr7TSTm$}DY%~SMpl>m{Sj!We+WfxSEor?YeiAxYUy25pn(?T()E>ByP^c@ zipwvWrhIK((R((VU+;@LmOnDu)ZXB3YArzzin!Z^0;PyJWnlfflo|q8(QY;o1*5CO z##hnkO{uynTMdk`~DOC#1 zdiYxQoy}=@7(ke#A8$YZZVtk4wo$8x28&I;cY3Ro-|kW=*yiiHgCLZeAr)UtVx>Tu z|LvL0hq|1-jC0I4x#>&QZCfrVB=zT!nR|~Uz`9%~2 znl{uZ{VEszW`Fad^q_HB!K9*|U-stK%?~;g?&&+12A}Rq$z($Bzuk^2X(Y=hF?-dQ ztc3DsQKI;qhWIV`99Q#R3xnU0AvY!i*BECj-z9l74|%O=V@nlv|qqC^r^-~C?E zGW%c|uYgnfJ(gjsTm_cIqcv*mYM{+i+&@F@+69ZQOK&u#v4oxUSQJ=tvqQ3W=*m;| z>SkBi8LYb-qRY7Sthh*0%3XAC%$z1rhOJzuX=PkTOa=DlocZUpE#KxVNH5)_4n=T( zGi3YrH7e~sPNYVBd~Grcq#CF~rN{p9Zza-Ntnwfma@TB)=3g36*0lSZg#ixEjFe%+ zX=&LDZ5zqculZ`=RYc^ln(~;nN|Qh6gN=!6f9-N2h+3NWbIxYud&;4SX*tWf5slk4 z{q@@l71UAZgj~*6edXb57fBUxvAS7s(RI=X868JM0+^DCn2yC>;v%S;qPOjB>YVsz(Zx9a>>BK&M zIQK>7_n)4ud0X5YM}^i*keH{ehLsiy9@NvOpsFeQjdI6anLGvVbBw_*fU1TzdVS$i z*4j7z!I5RF#rSz|8ibi$;qE{4`aqWYik7QB5U&F5C*;TO_x+gtzPGpzNt!7~nsBT7)Ckc(K~%uv&{{6A`mmBJVAk-{s~52Vu|HbCH7_W1~ZCX^RflOakGg=jo2Z z<*s;5-J+2@^LRDZ-7EV&Pq+FTErw@pfFqvx^i%E7Fx#^n(E`m2(c>K-O5`M`Yek9el zzTGs5qD6*G;y#~xu3>qWuO?-amKYtvRA}I9z#UspEeM;wOERYeot_n_EUMJf$4_u?E!6X~?q)tPoZb^_;8Y_Ox2h1m<+Le-fsRd|T8db<8#$bqez zua^Z|>h%zdnuU^ww$#-dZ9NTM`FN+!IlLkz*FqWb!x^Z|C{KyGjZ+>G;;7Mb@LY|H zc+Gp`L((Dw7pnDlHNm&;SfHedhx*kad$I^uGz{`0BYelq0yEUHpNKSkvj$|dpvY3{7*YGyhXA^LP0&wOw9oNoC=QoVx1<2Dne8qqZL zm>nFh5DX(-RnQwvHCZQwn^#Z=E!SPVlaRJ78Bo@}!!9dRt^qZy?-*`Pt4WSmgucJv zV1yFkcjlEM^uz-;b#Q7ZCP@Lk)m}uPX={R4B=56k7WNh11BN~0T*vr@!!ow^B0hOR zQ)4)&(e%>bNNL%bm<&8H{*l_L7s0$2GUgX2Vd;=4d9Dm2v3TaL+;L>{K7h7 zV#k?xDPm(NDE31$ z<}|X)pEY6myjK+^gaIMk&Yj2~F0rSKemNqlsVm4c|N7mp_C*L01s;GNx#D-*&gk!qQr}^?_r@q!8fuXw!)fA7xkd} zb>vHvdx~H$5qqAWrow7}+8zBM65-JOt5z za=T6f7MK`XJuQog8kIEboPdhcaVJeHy)5z7EBLK5NRr()E|#K0L0N^JD@pUA^Czb` zbUZ_558y+vqAGeyHCbrvOvLD67Ph}06959VzQ_|>RrXQAqE+AQ(-AaKdxoWaF8hdt z{O3W@b^*o#-f1VuU>YMV03ELF7zkCN4Q&b#prz%3Nne0lSbRo@@ z^ihv%oIl~Qyl6Q;a#$*jOC%x0_;eis*)J7=f@Ct*)xF5 zo}u~@-I}2|$b%5L7>@+Z?4o+1r&v6ceIy+vroK&jCQ<4q&45HP2wCol4hVm3pZtjf zHz1D7oyaSKJ~T{Gx}7ONLA)D5k(%%`WswrDyzX*rn}i}}TB4^y#@mAwPzoC)`?rYv zHgx|trUN#mu*VzUV~8TnJM2Qh*ZM5B{x&y>5An`(M7=Z*Q>TdiH@j*2=moNuOtvpz z+G`@~-`%~+AgPKgke@XiRPgndh@bp*-HRsh;HTtz@-y_uhb%7ylVOTqG0#u?Vn5c5 zEp*XRo|8hcgG^$#{$O9CJ&NE;TrfRpSnLmes&MO{m=N%zc`}gb!eQ7odl$oy1%PI} z#AIxx%oRVy&{O~9xnK4$EY>(eQj}!HKIV$Fz*H=-=Kn)N0D6u`(;iO|VraI4fu_W` z;b5{7;Lyx4za}DU#+U7}=H0dAS#YJJ&g2!P@Htu-AL&w=-)*%P9h2{wR|@?Ff9~)b z^+e_3Hetq7W%ls{!?<6&Y$Z;NNB41pvrv)|MET6AZXFXJeFqbFW5@i5WGzl?bP+~? z*&_puH;wKv2)9T_d+P`bLvJFqX#j&xa*-;0nGBbQf0DC>o~=J_Wmtf*2SZQr?{i~X z9-IbRH8{iy?<0v9Ir1?$66+igy|yDQ5J~A9sFX@Pe<*kCY8+MwH?I z`P}zfQ6l^AO8ehZ=l^ZR;R%uu4;BK*=?W9t|0{+-at(MQZ(CtG=EJFNaFMlKCMXu30(gJUqj5+ z`GM|!keqcj;FKTa_qq;{*dHRXAq157hlB@kL#8%yAm2AgfU|*rDKX@FLlp=HL8ddv zAWLCHe@DcDeB2}fl7#=0+#<05c3=VqM*O3bkr@9X4nO|)q0hU;Gye{L8ZN*NH8Id@mP-u;Fmb8YuorjLrW&ndip8CN%_qp982r w1WEnz9^$&s1hkp_3#lPJQ~!HI7WYYjA7>z!`?f%npAh2%rB@vD|Lau$2O)#1n*aa+ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 78e8b07ec0..5b6fb15e17 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionSha256Sum=9afb3ca688fc12c761a0e9e4321e4d24e977a4a8916c8a768b1fe05ddb4d6b66 +distributionSha256Sum=e5444a57cda4a95f90b0c9446a9e1b47d3d7f69057765bfb54bd4f482542d548 From 56864167a2c14269d4554f73c569dd76b44726f3 Mon Sep 17 00:00:00 2001 From: Mat Jaggard Date: Wed, 16 Mar 2022 12:37:26 +0000 Subject: [PATCH 076/737] GH-1436: Async Container Stop Resolves https://github.com/spring-projects/spring-amqp/issues/1436 Allow shutdown to be started but waiting to be completed asynchronously Use Task Executor from parent Fix imports Fix imports --- .../SimpleMessageListenerContainer.java | 100 +++++++++++------- 1 file changed, 61 insertions(+), 39 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java index f0cd3ee91f..7b05c7f684 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -77,6 +77,7 @@ * @author Gary Russell * @author Artem Bilan * @author Alex Panchenko + * @author Mat Jaggard * * @since 1.0 */ @@ -605,59 +606,80 @@ private void waitForConsumersToStart(Set process @Override protected void doShutdown() { + shutdownAndWaitOrCallback(null); + } + + @Override + public void stop(Runnable callback) { + shutdownAndWaitOrCallback(callback); + } + + private void shutdownAndWaitOrCallback(@Nullable Runnable callback) { Thread thread = this.containerStoppingForAbort.get(); if (thread != null && !thread.equals(Thread.currentThread())) { logger.info("Shutdown ignored - container is stopping due to an aborted consumer"); return; } - try { - List canceledConsumers = new ArrayList<>(); - synchronized (this.consumersMonitor) { - if (this.consumers != null) { - Iterator consumerIterator = this.consumers.iterator(); - while (consumerIterator.hasNext()) { - BlockingQueueConsumer consumer = consumerIterator.next(); - consumer.basicCancel(true); - canceledConsumers.add(consumer); - consumerIterator.remove(); - if (consumer.declaring) { - consumer.thread.interrupt(); - } + List canceledConsumers = new ArrayList<>(); + synchronized (this.consumersMonitor) { + if (this.consumers != null) { + Iterator consumerIterator = this.consumers.iterator(); + while (consumerIterator.hasNext()) { + BlockingQueueConsumer consumer = consumerIterator.next(); + consumer.basicCancel(true); + canceledConsumers.add(consumer); + consumerIterator.remove(); + if (consumer.declaring) { + consumer.thread.interrupt(); } } + } + else { + logger.info("Shutdown ignored - container is already stopped"); + return; + } + } + + Runnable awaitShutdown = () -> { + logger.info("Waiting for workers to finish."); + try { + boolean finished = this.cancellationLock.await(getShutdownTimeout(), TimeUnit.MILLISECONDS); + if (finished) { + logger.info("Successfully waited for workers to finish."); + } else { - logger.info("Shutdown ignored - container is already stopped"); - return; + logger.info("Workers not finished."); + if (isForceCloseChannel()) { + canceledConsumers.forEach(consumer -> { + if (logger.isWarnEnabled()) { + logger.warn("Closing channel for unresponsive consumer: " + consumer); + } + consumer.stop(); + }); + } } } - logger.info("Waiting for workers to finish."); - boolean finished = this.cancellationLock.await(getShutdownTimeout(), TimeUnit.MILLISECONDS); - if (finished) { - logger.info("Successfully waited for workers to finish."); + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.warn("Interrupted waiting for workers. Continuing with shutdown."); } - else { - logger.info("Workers not finished."); - if (isForceCloseChannel()) { - canceledConsumers.forEach(consumer -> { - if (logger.isWarnEnabled()) { - logger.warn("Closing channel for unresponsive consumer: " + consumer); - } - consumer.stop(); - }); - } + + synchronized (this.consumersMonitor) { + this.consumers = null; + this.cancellationLock.deactivate(); } - } - catch (InterruptedException e) { - Thread.currentThread().interrupt(); - logger.warn("Interrupted waiting for workers. Continuing with shutdown."); - } - synchronized (this.consumersMonitor) { - this.consumers = null; - this.cancellationLock.deactivate(); + if (callback != null) { + callback.run(); + } + }; + if (callback == null) { + awaitShutdown.run(); + } + else { + getTaskExecutor().execute(awaitShutdown); } - } private boolean isActive(BlockingQueueConsumer consumer) { From 1bc4ae477d5664fb28437ed999db791e26b7d757 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 21 Mar 2022 14:16:50 -0400 Subject: [PATCH 077/737] Revert "GH-1436: Async Container Stop" This reverts commit 56864167a2c14269d4554f73c569dd76b44726f3. --- .../SimpleMessageListenerContainer.java | 100 +++++++----------- 1 file changed, 39 insertions(+), 61 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java index 7b05c7f684..f0cd3ee91f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2021 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. @@ -77,7 +77,6 @@ * @author Gary Russell * @author Artem Bilan * @author Alex Panchenko - * @author Mat Jaggard * * @since 1.0 */ @@ -606,80 +605,59 @@ private void waitForConsumersToStart(Set process @Override protected void doShutdown() { - shutdownAndWaitOrCallback(null); - } - - @Override - public void stop(Runnable callback) { - shutdownAndWaitOrCallback(callback); - } - - private void shutdownAndWaitOrCallback(@Nullable Runnable callback) { Thread thread = this.containerStoppingForAbort.get(); if (thread != null && !thread.equals(Thread.currentThread())) { logger.info("Shutdown ignored - container is stopping due to an aborted consumer"); return; } - List canceledConsumers = new ArrayList<>(); - synchronized (this.consumersMonitor) { - if (this.consumers != null) { - Iterator consumerIterator = this.consumers.iterator(); - while (consumerIterator.hasNext()) { - BlockingQueueConsumer consumer = consumerIterator.next(); - consumer.basicCancel(true); - canceledConsumers.add(consumer); - consumerIterator.remove(); - if (consumer.declaring) { - consumer.thread.interrupt(); + try { + List canceledConsumers = new ArrayList<>(); + synchronized (this.consumersMonitor) { + if (this.consumers != null) { + Iterator consumerIterator = this.consumers.iterator(); + while (consumerIterator.hasNext()) { + BlockingQueueConsumer consumer = consumerIterator.next(); + consumer.basicCancel(true); + canceledConsumers.add(consumer); + consumerIterator.remove(); + if (consumer.declaring) { + consumer.thread.interrupt(); + } } } - } - else { - logger.info("Shutdown ignored - container is already stopped"); - return; - } - } - - Runnable awaitShutdown = () -> { - logger.info("Waiting for workers to finish."); - try { - boolean finished = this.cancellationLock.await(getShutdownTimeout(), TimeUnit.MILLISECONDS); - if (finished) { - logger.info("Successfully waited for workers to finish."); - } else { - logger.info("Workers not finished."); - if (isForceCloseChannel()) { - canceledConsumers.forEach(consumer -> { - if (logger.isWarnEnabled()) { - logger.warn("Closing channel for unresponsive consumer: " + consumer); - } - consumer.stop(); - }); - } + logger.info("Shutdown ignored - container is already stopped"); + return; } } - catch (InterruptedException e) { - Thread.currentThread().interrupt(); - logger.warn("Interrupted waiting for workers. Continuing with shutdown."); - } - - synchronized (this.consumersMonitor) { - this.consumers = null; - this.cancellationLock.deactivate(); + logger.info("Waiting for workers to finish."); + boolean finished = this.cancellationLock.await(getShutdownTimeout(), TimeUnit.MILLISECONDS); + if (finished) { + logger.info("Successfully waited for workers to finish."); } - - if (callback != null) { - callback.run(); + else { + logger.info("Workers not finished."); + if (isForceCloseChannel()) { + canceledConsumers.forEach(consumer -> { + if (logger.isWarnEnabled()) { + logger.warn("Closing channel for unresponsive consumer: " + consumer); + } + consumer.stop(); + }); + } } - }; - if (callback == null) { - awaitShutdown.run(); } - else { - getTaskExecutor().execute(awaitShutdown); + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.warn("Interrupted waiting for workers. Continuing with shutdown."); + } + + synchronized (this.consumersMonitor) { + this.consumers = null; + this.cancellationLock.deactivate(); } + } private boolean isActive(BlockingQueueConsumer consumer) { From ebe3ba8d0fdf948cd0c4e3c791e84877026de54b Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 21 Mar 2022 14:43:56 -0400 Subject: [PATCH 078/737] Revert "Upgrade Gradle" This reverts commit 65edbde6cb5091979d3a8ef5b9f7441b17d00047. --- gradle/wrapper/gradle-wrapper.jar | Bin 59821 -> 59536 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 41d9927a4d4fb3f96a785543079b8df6723c946b..7454180f2ae8848c63b8b4dea2cb829da983f2fa 100644 GIT binary patch delta 8722 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 delta 8958 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 Date: Mon, 21 Mar 2022 14:56:48 -0400 Subject: [PATCH 079/737] Exclude Micrometer from spring-data --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index d1ca227d5a..4a780c7599 100644 --- a/build.gradle +++ b/build.gradle @@ -358,6 +358,7 @@ project('spring-amqp') { // Spring Data projection message binding support optionalApi ("org.springframework.data:spring-data-commons:$springDataCommonsVersion") { exclude group: 'org.springframework' + exclude group: 'io.micrometer' } optionalApi "com.jayway.jsonpath:json-path:$jaywayJsonPathVersion" From f50c2e8df3fea35cf3ff7927560329852daf16f1 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 21 Mar 2022 19:19:05 +0000 Subject: [PATCH 080/737] [artifactory-release] Release version 3.0.0-M2 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 2477c6a3c5..8d3eeabb05 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.0-SNAPSHOT +version=3.0.0-M2 org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true From 1161579804c17a7499ac4281a45ae98332ce8378 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 21 Mar 2022 19:19:07 +0000 Subject: [PATCH 081/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 8d3eeabb05..2477c6a3c5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.0-M2 +version=3.0.0-SNAPSHOT org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true From d6b41355264a9968c5b070932050e60fec0b0b1f Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 22 Mar 2022 09:51:14 -0400 Subject: [PATCH 082/737] Upgrade to spring-data M3 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4a780c7599..a1f0a3533d 100644 --- a/build.gradle +++ b/build.gradle @@ -62,7 +62,7 @@ ext { rabbitmqHttpClientVersion = '3.12.1' reactorVersion = '2020.0.17' snappyVersion = '1.1.8.4' - springDataCommonsVersion = '3.0.0-M2' + springDataCommonsVersion = '3.0.0-M3' springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.0-M3' springRetryVersion = '1.3.2' zstdJniVersion = '1.5.0-2' From a6d9202c9bada25fe5475b34c4ef055073243aba Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 23 Mar 2022 13:20:30 -0400 Subject: [PATCH 083/737] GH-1439: Fix Memory Leak with Misconfiguration Resolves https://github.com/spring-projects/spring-amqp/issues/1439 Do not store pending confirms/returns if `RabbitTemplate` has confirms enabled but the factory does not support confirms. * Test polishing; include a returned message. **cherry-pick to `2.4.x` & `2.3.x`** --- .../amqp/rabbit/core/RabbitTemplate.java | 25 ++++++++------ ...atePublisherCallbacksIntegrationTests.java | 33 ++++++++++++++++++- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java index a5f030d1d5..fa0f8d47fc 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java @@ -2437,17 +2437,22 @@ protected void sendToRabbit(Channel channel, String exchange, String routingKey, private void setupConfirm(Channel channel, Message message, @Nullable CorrelationData correlationDataArg) { if ((this.publisherConfirms || this.confirmCallback != null) && channel instanceof PublisherCallbackChannel) { - PublisherCallbackChannel publisherCallbackChannel = (PublisherCallbackChannel) channel; - CorrelationData correlationData = this.correlationDataPostProcessor != null - ? this.correlationDataPostProcessor.postProcess(message, correlationDataArg) - : correlationDataArg; long nextPublishSeqNo = channel.getNextPublishSeqNo(); - message.getMessageProperties().setPublishSequenceNumber(nextPublishSeqNo); - publisherCallbackChannel.addPendingConfirm(this, nextPublishSeqNo, - new PendingConfirm(correlationData, System.currentTimeMillis())); - if (correlationData != null && StringUtils.hasText(correlationData.getId())) { - message.getMessageProperties().setHeader(PublisherCallbackChannel.RETURNED_MESSAGE_CORRELATION_KEY, - correlationData.getId()); + if (nextPublishSeqNo > 0) { + PublisherCallbackChannel publisherCallbackChannel = (PublisherCallbackChannel) channel; + CorrelationData correlationData = this.correlationDataPostProcessor != null + ? this.correlationDataPostProcessor.postProcess(message, correlationDataArg) + : correlationDataArg; + message.getMessageProperties().setPublishSequenceNumber(nextPublishSeqNo); + publisherCallbackChannel.addPendingConfirm(this, nextPublishSeqNo, + new PendingConfirm(correlationData, System.currentTimeMillis())); + if (correlationData != null && StringUtils.hasText(correlationData.getId())) { + message.getMessageProperties().setHeader(PublisherCallbackChannel.RETURNED_MESSAGE_CORRELATION_KEY, + correlationData.getId()); + } + } + else { + logger.debug("Factory does not have confirms enabled"); } } else if (channel instanceof ChannelProxy && ((ChannelProxy) channel).isConfirmSelected()) { diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests.java index 0ac94da528..2c9808fefe 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -48,6 +48,7 @@ import java.util.concurrent.atomic.AtomicReference; import org.apache.commons.logging.Log; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -130,6 +131,7 @@ public void create() { connectionFactoryWithReturnsEnabled.setPort(BrokerTestUtils.getPort()); connectionFactoryWithReturnsEnabled.setPublisherReturns(true); templateWithReturnsEnabled = new RabbitTemplate(connectionFactoryWithReturnsEnabled); + templateWithReturnsEnabled.setMandatory(true); connectionFactoryWithConfirmsAndReturnsEnabled = new CachingConnectionFactory(); connectionFactoryWithConfirmsAndReturnsEnabled.setHost("localhost"); connectionFactoryWithConfirmsAndReturnsEnabled.setChannelCacheSize(100); @@ -320,6 +322,7 @@ public void testPublisherConfirmNotReceived() throws Exception { Connection mockConnection = mock(Connection.class); Channel mockChannel = mock(Channel.class); given(mockChannel.isOpen()).willReturn(true); + given(mockChannel.getNextPublishSeqNo()).willReturn(1L); given(mockConnectionFactory.newConnection(any(ExecutorService.class), anyString())).willReturn(mockConnection); given(mockConnection.isOpen()).willReturn(true); @@ -863,4 +866,32 @@ public void testWithFuture() throws Exception { admin.deleteQueue(queue.getName()); } + @Test + void justReturns() throws InterruptedException { + CorrelationData correlationData = new CorrelationData(); + CountDownLatch latch = new CountDownLatch(1); + this.templateWithReturnsEnabled.setReturnsCallback(returned -> { + latch.countDown(); + }); + this.templateWithReturnsEnabled.setConfirmCallback((correlationData1, ack, cause) -> { + // has callback but factory is not enabled + }); + this.templateWithReturnsEnabled.convertAndSend("", ROUTE, "foo", correlationData); + ChannelProxy channel = (ChannelProxy) this.connectionFactoryWithReturnsEnabled.createConnection() + .createChannel(false); + assertThat(channel.getTargetChannel()) + .extracting("pendingReturns") + .asInstanceOf(InstanceOfAssertFactories.MAP) + .isEmpty(); + assertThat(channel.getTargetChannel()) + .extracting("pendingConfirms") + .asInstanceOf(InstanceOfAssertFactories.MAP) + .extracting(map -> map.values().iterator().next()) + .asInstanceOf(InstanceOfAssertFactories.MAP) + .isEmpty(); + + this.templateWithReturnsEnabled.convertAndSend("", "___JUNK___", "foo", correlationData); + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + } + } From 38da8c76102cd00e7bb823f998f00d3a74e83b09 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 23 Mar 2022 17:17:33 -0400 Subject: [PATCH 084/737] GH-1441: Fix Payload Detection with MessageHeaders Resolves https://github.com/spring-projects/spring-amqp/issues/1441 Previously, `MessageHeaders` had to be annotated with `@Headers` so that it was ignored during payload parameter resolution; otherwise it caused ambiguity. Ignore `MessageHeaders` even when not so annotated. Also fix some tests that were checking the same topic and count down latch so were unconditionally passing. Change one of those tests to verify the fix. **cherry-pick to 2.4.x, 2.3.x** --- .../MessagingMessageListenerAdapter.java | 6 ++-- .../EnableRabbitIntegrationTests.java | 30 ++++++++++++------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java index 4084a6ba94..a409ec4232 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -34,6 +34,7 @@ import org.springframework.core.MethodParameter; import org.springframework.lang.Nullable; import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.MessagingException; import org.springframework.messaging.handler.annotation.Header; import org.springframework.messaging.handler.annotation.Headers; @@ -378,7 +379,8 @@ private Type determineInferredType() { // NOSONAR - complexity * We ignore parameters with type Message because they are not involved with conversion. */ boolean isHeaderOrHeaders = methodParameter.hasParameterAnnotation(Header.class) - || methodParameter.hasParameterAnnotation(Headers.class); + || methodParameter.hasParameterAnnotation(Headers.class) + || methodParameter.getParameterType().equals(MessageHeaders.class); boolean isPayload = methodParameter.hasParameterAnnotation(Payload.class); if (isHeaderOrHeaders && isPayload && MessagingMessageListenerAdapter.this.logger.isWarnEnabled()) { MessagingMessageListenerAdapter.this.logger.warn(this.method.getName() diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java index 850cf1104e..3b2bee26b4 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java @@ -120,6 +120,7 @@ import org.springframework.core.task.TaskExecutor; import org.springframework.data.web.JsonPath; import org.springframework.lang.NonNull; +import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.converter.GenericMessageConverter; import org.springframework.messaging.handler.annotation.Header; import org.springframework.messaging.handler.annotation.Payload; @@ -560,8 +561,9 @@ public void testRabbitHandlerNoDefaultValidationCount() throws InterruptedExcept public void testDifferentTypes() throws InterruptedException { Foo1 foo = new Foo1(); foo.setBar("bar"); + this.service.foos.clear(); this.jsonRabbitTemplate.convertAndSend("differentTypes", foo); - assertThat(this.service.latch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(this.service.dtLatch1.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(this.service.foos.get(0)).isInstanceOf(Foo2.class); assertThat(((Foo2) this.service.foos.get(0)).getBar()).isEqualTo("bar"); assertThat(TestUtils.getPropertyValue(this.registry.getListenerContainer("different"), "concurrentConsumers")).isEqualTo(2); @@ -571,8 +573,9 @@ public void testDifferentTypes() throws InterruptedException { public void testDifferentTypesWithConcurrency() throws InterruptedException { Foo1 foo = new Foo1(); foo.setBar("bar"); - this.jsonRabbitTemplate.convertAndSend("differentTypes", foo); - assertThat(this.service.latch.await(10, TimeUnit.SECONDS)).isTrue(); + this.service.foos.clear(); + this.jsonRabbitTemplate.convertAndSend("differentTypes2", foo); + assertThat(this.service.dtLatch2.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(this.service.foos.get(0)).isInstanceOf(Foo2.class); assertThat(((Foo2) this.service.foos.get(0)).getBar()).isEqualTo("bar"); MessageListenerContainer container = this.registry.getListenerContainer("differentWithConcurrency"); @@ -584,8 +587,9 @@ public void testDifferentTypesWithConcurrency() throws InterruptedException { public void testDifferentTypesWithVariableConcurrency() throws InterruptedException { Foo1 foo = new Foo1(); foo.setBar("bar"); - this.jsonRabbitTemplate.convertAndSend("differentTypes", foo); - assertThat(this.service.latch.await(10, TimeUnit.SECONDS)).isTrue(); + this.service.foos.clear(); + this.jsonRabbitTemplate.convertAndSend("differentTypes3", foo); + assertThat(this.service.dtLatch3.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(this.service.foos.get(0)).isInstanceOf(Foo2.class); assertThat(((Foo2) this.service.foos.get(0)).getBar()).isEqualTo("bar"); MessageListenerContainer container = this.registry.getListenerContainer("differentWithVariableConcurrency"); @@ -1086,7 +1090,11 @@ public static class MyService { final List foos = new ArrayList<>(); - final CountDownLatch latch = new CountDownLatch(1); + final CountDownLatch dtLatch1 = new CountDownLatch(1); + + final CountDownLatch dtLatch2 = new CountDownLatch(1); + + final CountDownLatch dtLatch3 = new CountDownLatch(1); final CountDownLatch validationLatch = new CountDownLatch(1); @@ -1237,21 +1245,21 @@ public void handleIt(Date body) { containerFactory = "jsonListenerContainerFactoryNoClassMapper") public void handleDifferent(@Validated Foo2 foo) { foos.add(foo); - latch.countDown(); + dtLatch1.countDown(); } @RabbitListener(id = "differentWithConcurrency", queues = "differentTypes2", - containerFactory = "jsonListenerContainerFactory", concurrency = "#{3}") - public void handleDifferentWithConcurrency(Foo2 foo) { + containerFactory = "jsonListenerContainerFactoryNoClassMapper", concurrency = "#{3}") + public void handleDifferentWithConcurrency(Foo2 foo, MessageHeaders headers) { foos.add(foo); - latch.countDown(); + dtLatch2.countDown(); } @RabbitListener(id = "differentWithVariableConcurrency", queues = "differentTypes3", containerFactory = "jsonListenerContainerFactory", concurrency = "3-4") public void handleDifferentWithVariableConcurrency(Foo2 foo) { foos.add(foo); - latch.countDown(); + dtLatch3.countDown(); } @RabbitListener(id = "notStarted", containerFactory = "rabbitAutoStartFalseListenerContainerFactory", From ea015663ca69654e26c642c6dc55cad1ab1ae92c Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 29 Mar 2022 12:41:45 -0400 Subject: [PATCH 085/737] GH-1443: Pull CCF.resetConnection() to CF Resolves https://github.com/spring-projects/spring-amqp/issues/1443 **Cherry-pick to `2.4.x`** --- .../AbstractRoutingConnectionFactory.java | 14 +++++++++++++- .../connection/CachingConnectionFactory.java | 3 ++- .../amqp/rabbit/connection/ConnectionFactory.java | 10 +++++++++- .../LocalizedQueueConnectionFactory.java | 9 +++++++-- .../connection/PooledChannelConnectionFactory.java | 1 + .../connection/ThreadChannelConnectionFactory.java | 3 ++- 6 files changed, 34 insertions(+), 6 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.java index 5cfa2adc78..2ea39028c9 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.java @@ -22,6 +22,7 @@ import java.util.concurrent.ConcurrentHashMap; import org.springframework.amqp.AmqpException; +import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -38,7 +39,7 @@ * @since 1.3 */ public abstract class AbstractRoutingConnectionFactory implements ConnectionFactory, RoutingConnectionFactory, - InitializingBean { + InitializingBean, DisposableBean { private final Map targetConnectionFactories = new ConcurrentHashMap(); @@ -260,4 +261,15 @@ protected ConnectionFactory removeTargetConnectionFactory(Object key) { @Nullable protected abstract Object determineCurrentLookupKey(); + @Override + public void destroy() { + resetConnection(); + } + + @Override + public void resetConnection() { + this.targetConnectionFactories.values().forEach(factory -> factory.resetConnection()); + this.defaultTargetConnectionFactory.resetConnection(); + } + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java index 0fa960684b..15504f2202 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -882,6 +882,7 @@ public final void destroy() { * used to force a reconnect to the primary broker after failing over to a secondary * broker. */ + @Override public void resetConnection() { synchronized (this.connectionMonitor) { if (this.connection.target != null) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactory.java index 4b1e049be6..2f4323b540 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -83,4 +83,12 @@ default boolean isPublisherReturns() { return false; } + /** + * Close any connection(s) that might be cached by this factory. This does not prevent + * new connections from being opened. + * @since 2.4.4 + */ + default void resetConnection() { + } + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java index fc04a7319d..e209734ec6 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-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. @@ -350,7 +350,7 @@ protected ConnectionFactory createConnectionFactory(String address, String node) } @Override - public void destroy() { + public void resetConnection() { Exception lastException = null; for (ConnectionFactory connectionFactory : this.nodeFactories.values()) { if (connectionFactory instanceof DisposableBean) { @@ -367,4 +367,9 @@ public void destroy() { } } + @Override + public void destroy() { + resetConnection(); + } + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java index 391874f301..15add4091f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java @@ -149,6 +149,7 @@ public synchronized Connection createConnection() throws AmqpException { * used to force a reconnect to the primary broker after failing over to a secondary * broker. */ + @Override public void resetConnection() { destroy(); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java index 35a28687d8..0da501fe84 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-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. @@ -140,6 +140,7 @@ public void closeThreadChannel() { * used to force a reconnect to the primary broker after failing over to a secondary * broker. */ + @Override public void resetConnection() { destroy(); } From 6eca017a187268e24035ef425039c17e85447800 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 29 Mar 2022 12:55:41 -0400 Subject: [PATCH 086/737] GH-1444: Move to Micrometer Snapshots https://github.com/spring-projects/spring-amqp/issues/1444 Phase 0 --- build.gradle | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index a1f0a3533d..e431c4e673 100644 --- a/build.gradle +++ b/build.gradle @@ -55,14 +55,15 @@ ext { log4jVersion = '2.17.1' logbackVersion = '1.2.3' lz4Version = '1.8.0' - micrometerVersion = '2.0.0-M3' + micrometerVersion = '2.0.0-SNAPSHOT' + micrometerTracingVersion = '1.0.0-SNAPSHOT' mockitoVersion = '4.0.0' rabbitmqStreamVersion = '0.4.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.13.1' rabbitmqHttpClientVersion = '3.12.1' reactorVersion = '2020.0.17' snappyVersion = '1.1.8.4' - springDataCommonsVersion = '3.0.0-M3' + springDataVersion = '2022.0.0-M3' springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.0-M3' springRetryVersion = '1.3.2' zstdJniVersion = '1.5.0-2' @@ -93,6 +94,9 @@ allprojects { mavenBom "org.springframework:spring-framework-bom:$springVersion" mavenBom "io.projectreactor:reactor-bom:$reactorVersion" mavenBom "org.apache.logging.log4j:log4j-bom:$log4jVersion" + mavenBom "org.springframework.data:spring-data-bom:$springDataVersion" + mavenBom "io.micrometer:micrometer-bom:$micrometerVersion" + mavenBom "io.micrometer:micrometer-tracing-bom:$micrometerTracingVersion" } } @@ -356,7 +360,7 @@ project('spring-amqp') { optionalApi 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' // Spring Data projection message binding support - optionalApi ("org.springframework.data:spring-data-commons:$springDataCommonsVersion") { + optionalApi ('org.springframework.data:spring-data-commons') { exclude group: 'org.springframework' exclude group: 'io.micrometer' } @@ -382,10 +386,10 @@ project('spring-rabbit') { optionalApi 'io.projectreactor:reactor-core' optionalApi "ch.qos.logback:logback-classic:$logbackVersion" optionalApi 'org.apache.logging.log4j:log4j-core' - optionalApi "io.micrometer:micrometer-core:$micrometerVersion" - optionalApi "io.micrometer:micrometer-binders:$micrometerVersion" + optionalApi 'io.micrometer:micrometer-binders' + optionalApi 'io.micrometer:micrometer-tracing-api' // Spring Data projection message binding support - optionalApi ("org.springframework.data:spring-data-commons:$springDataCommonsVersion") { + optionalApi ("org.springframework.data:spring-data-commons") { exclude group: 'org.springframework' } optionalApi "com.jayway.jsonpath:json-path:$jaywayJsonPathVersion" @@ -394,6 +398,9 @@ project('spring-rabbit') { testApi project(':spring-rabbit-junit') testImplementation("com.willowtreeapps.assertk:assertk-jvm:$assertkVersion") testImplementation "org.hibernate.validator:hibernate-validator:$hibernateValidationVersion" + testImplementation 'io.micrometer:micrometer-tracing-bridge-brave' + testImplementation 'io.micrometer:micrometer-tracing-test' + testImplementation 'io.micrometer:micrometer-tracing-integration-test' testRuntimeOnly 'org.springframework:spring-web' testRuntimeOnly "org.apache.httpcomponents:httpclient:$commonsHttpClientVersion" testRuntimeOnly 'com.fasterxml.jackson.core:jackson-core' From eccdb47155413fa8800849086810f965a6836c0d Mon Sep 17 00:00:00 2001 From: Leonardo Ferreira Date: Sat, 19 Mar 2022 15:43:31 -0300 Subject: [PATCH 087/737] GH-1434: Mixed CFs With/Without Confirms/Returns GH-1434 allowing to rabbit template have multiple connection factories with not same confirms and returns flags. GH-1434 avoiding call obtainTargetConnectionFactory twice GH-1434 test GH-1434 javadoc + removing else GH-1434 fixing checkstyle GH-1434 using publisherConfirms from PooledChannelConnectionFactory GH-1434 adapting AbstractRoutingConnectionFactory GH-1434 javadoc & checkstyle & BeforeEach > BeforeAll GH-1434 javadoc GH-1434 doc GH-1434 doc --- CONTRIBUTING.adoc | 2 +- .../AbstractRoutingConnectionFactory.java | 32 ++++- .../connection/CachingConnectionFactory.java | 4 + .../amqp/rabbit/connection/ChannelProxy.java | 8 ++ .../PooledChannelConnectionFactory.java | 3 + .../ThreadChannelConnectionFactory.java | 4 + .../amqp/rabbit/core/RabbitTemplate.java | 26 ++-- ...tingConnectionFactoryIntegrationTests.java | 123 ++++++++++++++++++ src/reference/asciidoc/amqp.adoc | 42 ++++++ 9 files changed, 224 insertions(+), 20 deletions(-) create mode 100644 spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateRoutingConnectionFactoryIntegrationTests.java diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc index 23425ec68a..849b95168b 100644 --- a/CONTRIBUTING.adoc +++ b/CONTRIBUTING.adoc @@ -51,7 +51,7 @@ _you should see branches on origin as well as upstream, including 'main' and 'ma == A Day in the Life of a Contributor -* _Always_ work on topic branches (Typically use the HitHub (or JIRA) issue ID as the branch name). +* _Always_ work on topic branches (Typically use the GitHub (or JIRA) issue ID as the branch name). - For example, to create and switch to a new branch for issue #123: `git checkout -b GH-123` * You might be working on several different topic branches at any given time, but when at a stopping point for one of those branches, commit (a local operation). * Please follow the "Commit Guidelines" described in https://git-scm.com/book/en/Distributed-Git-Contributing-to-a-Project[this chapter of Pro Git]. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.java index 2ea39028c9..fdf057a34b 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.java @@ -54,6 +54,8 @@ public abstract class AbstractRoutingConnectionFactory implements ConnectionFact private Boolean returns; + private boolean consistentConfirmsReturns = true; + /** * Specify the map of target ConnectionFactories, with the lookup key as key. *

The key can be of arbitrary type; this class implements the @@ -125,10 +127,13 @@ private void checkConfirmsAndReturns(ConnectionFactory cf) { if (this.returns == null) { this.returns = cf.isPublisherReturns(); } - Assert.isTrue(this.confirms.booleanValue() == cf.isPublisherConfirms(), - "Target connection factories must have the same setting for publisher confirms"); - Assert.isTrue(this.returns.booleanValue() == cf.isPublisherReturns(), - "Target connection factories must have the same setting for publisher returns"); + + if (this.consistentConfirmsReturns) { + Assert.isTrue(this.confirms.booleanValue() == cf.isPublisherConfirms(), + "Target connection factories must have the same setting for publisher confirms"); + Assert.isTrue(this.returns.booleanValue() == cf.isPublisherReturns(), + "Target connection factories must have the same setting for publisher returns"); + } } @Override @@ -230,6 +235,25 @@ public ConnectionFactory getTargetConnectionFactory(Object key) { return this.targetConnectionFactories.get(key); } + /** + * Specify whether to apply a validation enforcing all {@link ConnectionFactory#isPublisherConfirms()} and + * {@link ConnectionFactory#isPublisherReturns()} have a consistent value. + *

+ * A consistent value means that all ConnectionFactories must have the same value between all + * {@link ConnectionFactory#isPublisherConfirms()} and the same value between all + * {@link ConnectionFactory#isPublisherReturns()}. + *

+ *

+ * Note that in any case the values between {@link ConnectionFactory#isPublisherConfirms()} and + * {@link ConnectionFactory#isPublisherReturns()} don't need to be equals between each other. + *

+ * @param consistentConfirmsReturns true to validate, false to not validate. + * @since 2.4.4 + */ + public void setConsistentConfirmsReturns(boolean consistentConfirmsReturns) { + this.consistentConfirmsReturns = consistentConfirmsReturns; + } + /** * Adds the given {@link ConnectionFactory} and associates it with the given lookup key. * @param key the lookup key. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java index 15504f2202..78e6e5e944 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java @@ -94,6 +94,7 @@ * @author Artem Bilan * @author Steve Powell * @author Will Droste + * @author Leonardo Ferreira */ @ManagedResource public class CachingConnectionFactory extends AbstractConnectionFactory @@ -1133,6 +1134,9 @@ else if (methodName.equals("isTransactional")) { else if (methodName.equals("isConfirmSelected")) { return this.confirmSelected; } + else if (methodName.equals("isPublisherConfirms")) { + return this.publisherConfirms; + } try { if (this.target == null || !this.target.isOpen()) { if (this.target instanceof PublisherCallbackChannel) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ChannelProxy.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ChannelProxy.java index 17a423b7ba..e0afe0fa28 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ChannelProxy.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ChannelProxy.java @@ -54,4 +54,12 @@ default boolean isConfirmSelected() { return false; } + /** + * Return true if publisher confirms are enabled. + * @return true if publisherConfirms. + */ + default boolean isPublisherConfirms() { + return false; + } + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java index 15add4091f..186e0ca316 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java @@ -222,6 +222,8 @@ private Channel createProxy(Channel channel, boolean transacted) { return channel.confirmSelect(); case "isConfirmSelected": return confirmSelected.get(); + case "isPublisherConfirms": + return false; } return null; }; @@ -231,6 +233,7 @@ private Channel createProxy(Channel channel, boolean transacted) { advisor.addMethodName("isTransactional"); advisor.addMethodName("confirmSelect"); advisor.addMethodName("isConfirmSelected"); + advisor.addMethodName("isPublisherConfirms"); pf.addAdvisor(advisor); pf.addInterface(ChannelProxy.class); proxy.set((Channel) pf.getProxy()); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java index 0da501fe84..69edbc6456 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java @@ -44,6 +44,7 @@ * {@link #closeThreadChannel()}. * * @author Gary Russell + * @author Leonardo Ferreira * @since 2.3 * */ @@ -288,6 +289,8 @@ private Channel createProxy(Channel channel, boolean transactional) { return channel.confirmSelect(); case "isConfirmSelected": return confirmSelected.get(); + case "isPublisherConfirms": + return false; } return null; }; @@ -297,6 +300,7 @@ private Channel createProxy(Channel channel, boolean transactional) { advisor.addMethodName("isTransactional"); advisor.addMethodName("confirmSelect"); advisor.addMethodName("isConfirmSelected"); + advisor.addMethodName("isPublisherConfirms"); pf.addAdvisor(advisor); pf.addInterface(ChannelProxy.class); return (Channel) pf.getProxy(); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java index fa0f8d47fc..bca2597b5b 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java @@ -147,6 +147,7 @@ * @author Mark Norkin * @author Mohammad Hewedy * @author Alexey Platonov + * @author Leonardo Ferreira * * @since 1.0 */ @@ -257,10 +258,6 @@ public class RabbitTemplate extends RabbitAccessor // NOSONAR type line count private ErrorHandler replyErrorHandler; - private volatile Boolean confirmsOrReturnsCapable; - - private volatile boolean publisherConfirms; - private volatile boolean usingFastReplyTo; private volatile boolean evaluatedFastReplyTo; @@ -1263,7 +1260,7 @@ else if (isChannelTransacted()) { } return buildMessageFromDelivery(delivery); } - }); + }, obtainTargetConnectionFactory(this.receiveConnectionFactorySelectorExpression, null)); logReceived(message); return message; } @@ -1960,7 +1957,7 @@ private Message doSendAndReceiveWithDirect(String exchange, String routingKey, M boolean cancelConsumer = false; try { Channel channel = channelHolder.getChannel(); - if (this.confirmsOrReturnsCapable) { + if (isPublisherConfirmsOrReturns(connectionFactory)) { addListener(channel); } Message reply = doSendAndReceiveAsListener(exchange, routingKey, message, correlationData, channel, @@ -2224,12 +2221,10 @@ private void cleanUpAfterAction(@Nullable Channel channel, boolean invokeScope, private T invokeAction(ChannelCallback action, ConnectionFactory connectionFactory, Channel channel) throws Exception { // NOSONAR see the callback - if (this.confirmsOrReturnsCapable == null) { - determineConfirmsReturnsCapability(connectionFactory); - } - if (this.confirmsOrReturnsCapable) { + if (isPublisherConfirmsOrReturns(connectionFactory)) { addListener(channel); } + if (logger.isDebugEnabled()) { logger.debug( "Executing callback " + action.getClass().getSimpleName() + " on RabbitMQ Channel: " + channel); @@ -2351,10 +2346,8 @@ public void waitForConfirmsOrDie(long timeout) { } } - public void determineConfirmsReturnsCapability(ConnectionFactory connectionFactory) { - this.publisherConfirms = connectionFactory.isPublisherConfirms(); - this.confirmsOrReturnsCapable = - this.publisherConfirms || connectionFactory.isPublisherReturns(); + private boolean isPublisherConfirmsOrReturns(ConnectionFactory connectionFactory) { + return connectionFactory.isPublisherConfirms() || connectionFactory.isPublisherReturns(); } /** @@ -2435,8 +2428,11 @@ protected void sendToRabbit(Channel channel, String exchange, String routingKey, } private void setupConfirm(Channel channel, Message message, @Nullable CorrelationData correlationDataArg) { - if ((this.publisherConfirms || this.confirmCallback != null) && channel instanceof PublisherCallbackChannel) { + final boolean publisherConfirms = channel instanceof ChannelProxy + && ((ChannelProxy) channel).isPublisherConfirms(); + if ((publisherConfirms || this.confirmCallback != null) + && channel instanceof PublisherCallbackChannel) { long nextPublishSeqNo = channel.getNextPublishSeqNo(); if (nextPublishSeqNo > 0) { PublisherCallbackChannel publisherCallbackChannel = (PublisherCallbackChannel) channel; diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateRoutingConnectionFactoryIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateRoutingConnectionFactoryIntegrationTests.java new file mode 100644 index 0000000000..b2ebbbb7f1 --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateRoutingConnectionFactoryIntegrationTests.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-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.core; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageBuilder; +import org.springframework.amqp.rabbit.connection.AbstractRoutingConnectionFactory; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.connection.CorrelationData; +import org.springframework.amqp.rabbit.connection.PooledChannelConnectionFactory; +import org.springframework.amqp.rabbit.connection.SimpleRoutingConnectionFactory; +import org.springframework.amqp.rabbit.junit.BrokerTestUtils; +import org.springframework.amqp.rabbit.junit.RabbitAvailable; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +/** + * @author Leonardo Ferreira + * @since 2.4.4 + */ +@RabbitAvailable(queues = RabbitTemplateRoutingConnectionFactoryIntegrationTests.ROUTE) +class RabbitTemplateRoutingConnectionFactoryIntegrationTests { + + public static final String ROUTE = "test.queue.RabbitTemplateRoutingConnectionFactoryIntegrationTests"; + + private static RabbitTemplate rabbitTemplate; + + @BeforeAll + static void create() { + final com.rabbitmq.client.ConnectionFactory cf = new com.rabbitmq.client.ConnectionFactory(); + cf.setHost("localhost"); + cf.setPort(BrokerTestUtils.getPort()); + + CachingConnectionFactory cachingConnectionFactory = new CachingConnectionFactory(cf); + + cachingConnectionFactory.setPublisherConfirmType(CachingConnectionFactory.ConfirmType.CORRELATED); + + PooledChannelConnectionFactory pooledChannelConnectionFactory = new PooledChannelConnectionFactory(cf); + + Map connectionFactoryMap = new HashMap<>(2); + connectionFactoryMap.put("true", cachingConnectionFactory); + connectionFactoryMap.put("false", pooledChannelConnectionFactory); + + final AbstractRoutingConnectionFactory routingConnectionFactory = new SimpleRoutingConnectionFactory(); + routingConnectionFactory.setConsistentConfirmsReturns(false); + routingConnectionFactory.setDefaultTargetConnectionFactory(pooledChannelConnectionFactory); + routingConnectionFactory.setTargetConnectionFactories(connectionFactoryMap); + + rabbitTemplate = new RabbitTemplate(routingConnectionFactory); + + final Expression sendExpression = new SpelExpressionParser().parseExpression( + "messageProperties.headers['x-use-publisher-confirms'] ?: false"); + rabbitTemplate.setSendConnectionFactorySelectorExpression(sendExpression); + } + + @AfterAll + static void cleanUp() { + rabbitTemplate.destroy(); + } + + @Test + void sendWithoutConfirmsTest() { + final String payload = UUID.randomUUID().toString(); + rabbitTemplate.convertAndSend(ROUTE, (Object) payload, new CorrelationData()); + assertThat(rabbitTemplate.getUnconfirmedCount()).isZero(); + + final Message received = rabbitTemplate.receive(ROUTE, Duration.ofSeconds(3).toMillis()); + assertThat(received).isNotNull(); + final String receivedPayload = new String(received.getBody()); + + assertThat(receivedPayload).isEqualTo(payload); + } + + @Test + void sendWithConfirmsTest() throws Exception { + final String payload = UUID.randomUUID().toString(); + final Message message = MessageBuilder.withBody(payload.getBytes(StandardCharsets.UTF_8)) + .setHeader("x-use-publisher-confirms", "true").build(); + + final CorrelationData correlationData = new CorrelationData(); + rabbitTemplate.send(ROUTE, message, correlationData); + assertThat(rabbitTemplate.getUnconfirmedCount()).isEqualTo(1); + + final CorrelationData.Confirm confirm = correlationData.getFuture().get(10, TimeUnit.SECONDS); + + assertThat(confirm.isAck()).isTrue(); + + final Message received = rabbitTemplate.receive(ROUTE, Duration.ofSeconds(10).toMillis()); + assertThat(received).isNotNull(); + final String receivedPayload = new String(received.getBody()); + + assertThat(receivedPayload).isEqualTo(payload); + } + +} diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index 9c40006343..d2c5923db9 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -696,6 +696,48 @@ For example, with lookup key qualifier `thing1` and a container listening to que IMPORTANT: The target (and default, if provided) connection factories must have the same settings for publisher confirms and returns. See <>. +Starting with version 2.4.4, this validation can be disabled. +If you have a case that the values between confirms and returns need to be unequal, you can use `AbstractRoutingConnectionFactory#setConsistentConfirmsReturns` to turn of the validation. +Note that the first connection factory added to `AbstractRoutingConnectionFactory` will determine the general values of `confirms` and `returns`. + +It may be useful if you have a case that certain messages you would to check confirms/returns and others you don't. +For example: + +==== +[source, java] +---- +@Bean +public RabbitTemplate rabbitTemplate() { + final com.rabbitmq.client.ConnectionFactory cf = new com.rabbitmq.client.ConnectionFactory(); + cf.setHost("localhost"); + cf.setPort(5672); + + CachingConnectionFactory cachingConnectionFactory = new CachingConnectionFactory(cf); + cachingConnectionFactory.setPublisherConfirmType(CachingConnectionFactory.ConfirmType.CORRELATED); + + PooledChannelConnectionFactory pooledChannelConnectionFactory = new PooledChannelConnectionFactory(cf); + + final Map connectionFactoryMap = new HashMap<>(2); + connectionFactoryMap.put("true", cachingConnectionFactory); + connectionFactoryMap.put("false", pooledChannelConnectionFactory); + + final AbstractRoutingConnectionFactory routingConnectionFactory = new SimpleRoutingConnectionFactory(); + routingConnectionFactory.setConsistentConfirmsReturns(false); + routingConnectionFactory.setDefaultTargetConnectionFactory(pooledChannelConnectionFactory); + routingConnectionFactory.setTargetConnectionFactories(connectionFactoryMap); + + final RabbitTemplate rabbitTemplate = new RabbitTemplate(routingConnectionFactory); + + final Expression sendExpression = new SpelExpressionParser().parseExpression( + "messageProperties.headers['x-use-publisher-confirms'] ?: false"); + rabbitTemplate.setSendConnectionFactorySelectorExpression(sendExpression); +} +---- +==== + +This way messages with the header `x-use-publisher-confirms: true` will be sent through the caching connection and you can ensure the message delivery. +See <> for more information about ensuring message delivery. + [[queue-affinity]] ===== Queue Affinity and the `LocalizedQueueConnectionFactory` From 15336457dc96f5a73ac4bccea32c6dd1e85a1e26 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 30 Mar 2022 16:06:19 -0400 Subject: [PATCH 088/737] Upgrade Jackson Version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e431c4e673..8dfe94b081 100644 --- a/build.gradle +++ b/build.gradle @@ -48,7 +48,7 @@ ext { googleJsr305Version = '3.0.2' hamcrestVersion = '2.2' hibernateValidationVersion = '6.2.3.Final' - jacksonBomVersion = '2.13.2' + jacksonBomVersion = '2.13.2.20220328' jaywayJsonPathVersion = '2.6.0' junit4Version = '4.13.2' junitJupiterVersion = '5.8.2' From ff67612aefb78009c5b2251ba6dc1307f4d1d823 Mon Sep 17 00:00:00 2001 From: Mat Jaggard Date: Wed, 16 Mar 2022 12:37:26 +0000 Subject: [PATCH 089/737] GH-1436: Async Stop Containers Resolves https://github.com/spring-projects/spring-amqp/issues/1436 Allow shutdown to be started but waiting to be completed asynchronously Use Task Executor from parent Update abstract parent to allow running to be set to false --- .../AbstractMessageListenerContainer.java | 30 ++--- .../SimpleMessageListenerContainer.java | 103 +++++++++++------- 2 files changed, 75 insertions(+), 58 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index 8f0dfe693f..17b2beb1c6 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -109,6 +109,7 @@ * @author Arnaud Cogoluègnes * @author Artem Bilan * @author Mohammad Hewedy + * @author Mat Jaggard */ public abstract class AbstractMessageListenerContainer extends RabbitAccessor implements MessageListenerContainer, ApplicationContextAware, BeanNameAware, DisposableBean, @@ -1331,10 +1332,14 @@ public void shutdown() { throw convertRabbitAccessException(ex); } finally { - synchronized (this.lifecycleMonitor) { - this.running = false; - this.lifecycleMonitor.notifyAll(); - } + setNotRunning(); + } + } + + protected void setNotRunning() { + synchronized (this.lifecycleMonitor) { + this.running = false; + this.lifecycleMonitor.notifyAll(); } } @@ -1420,20 +1425,7 @@ public void stop() { throw convertRabbitAccessException(ex); } finally { - synchronized (this.lifecycleMonitor) { - this.running = false; - this.lifecycleMonitor.notifyAll(); - } - } - } - - @Override - public void stop(Runnable callback) { - try { - stop(); - } - finally { - callback.run(); + setNotRunning(); } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java index f0cd3ee91f..ed9cfc518e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -77,6 +77,7 @@ * @author Gary Russell * @author Artem Bilan * @author Alex Panchenko + * @author Mat Jaggard * * @since 1.0 */ @@ -605,59 +606,83 @@ private void waitForConsumersToStart(Set process @Override protected void doShutdown() { + shutdownAndWaitOrCallback(null); + } + + @Override + public void stop(Runnable callback) { + shutdownAndWaitOrCallback(() -> { + setNotRunning(); + callback.run(); + }); + } + + private void shutdownAndWaitOrCallback(@Nullable Runnable callback) { Thread thread = this.containerStoppingForAbort.get(); if (thread != null && !thread.equals(Thread.currentThread())) { logger.info("Shutdown ignored - container is stopping due to an aborted consumer"); return; } - try { - List canceledConsumers = new ArrayList<>(); - synchronized (this.consumersMonitor) { - if (this.consumers != null) { - Iterator consumerIterator = this.consumers.iterator(); - while (consumerIterator.hasNext()) { - BlockingQueueConsumer consumer = consumerIterator.next(); - consumer.basicCancel(true); - canceledConsumers.add(consumer); - consumerIterator.remove(); - if (consumer.declaring) { - consumer.thread.interrupt(); - } + List canceledConsumers = new ArrayList<>(); + synchronized (this.consumersMonitor) { + if (this.consumers != null) { + Iterator consumerIterator = this.consumers.iterator(); + while (consumerIterator.hasNext()) { + BlockingQueueConsumer consumer = consumerIterator.next(); + consumer.basicCancel(true); + canceledConsumers.add(consumer); + consumerIterator.remove(); + if (consumer.declaring) { + consumer.thread.interrupt(); } } + } + else { + logger.info("Shutdown ignored - container is already stopped"); + return; + } + } + + Runnable awaitShutdown = () -> { + logger.info("Waiting for workers to finish."); + try { + boolean finished = this.cancellationLock.await(getShutdownTimeout(), TimeUnit.MILLISECONDS); + if (finished) { + logger.info("Successfully waited for workers to finish."); + } else { - logger.info("Shutdown ignored - container is already stopped"); - return; + logger.info("Workers not finished."); + if (isForceCloseChannel()) { + canceledConsumers.forEach(consumer -> { + if (logger.isWarnEnabled()) { + logger.warn("Closing channel for unresponsive consumer: " + consumer); + } + consumer.stop(); + }); + } } } - logger.info("Waiting for workers to finish."); - boolean finished = this.cancellationLock.await(getShutdownTimeout(), TimeUnit.MILLISECONDS); - if (finished) { - logger.info("Successfully waited for workers to finish."); + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.warn("Interrupted waiting for workers. Continuing with shutdown."); } - else { - logger.info("Workers not finished."); - if (isForceCloseChannel()) { - canceledConsumers.forEach(consumer -> { - if (logger.isWarnEnabled()) { - logger.warn("Closing channel for unresponsive consumer: " + consumer); - } - consumer.stop(); - }); - } + + synchronized (this.consumersMonitor) { + this.consumers = null; + this.cancellationLock.deactivate(); } - } - catch (InterruptedException e) { - Thread.currentThread().interrupt(); - logger.warn("Interrupted waiting for workers. Continuing with shutdown."); - } - synchronized (this.consumersMonitor) { - this.consumers = null; - this.cancellationLock.deactivate(); + if (callback != null) { + callback.run(); + } + }; + if (callback == null) { + awaitShutdown.run(); + } + else { + getTaskExecutor().execute(awaitShutdown); } - } private boolean isActive(BlockingQueueConsumer consumer) { From de6ab797c15270264782e8a5518d551ff639a03e Mon Sep 17 00:00:00 2001 From: Yann Bolliger Date: Tue, 5 Apr 2022 10:05:50 +0200 Subject: [PATCH 090/737] Fix typo in amqp.adoc --- src/reference/asciidoc/amqp.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index d2c5923db9..e3c2b9beb0 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -364,7 +364,7 @@ A `ConnectionFactory` can be created quickly and conveniently by using the rabbi In most cases, this approach is preferable, since the framework can choose the best defaults for you. The created instance is a `CachingConnectionFactory`. Keep in mind that the default cache size for channels is 25. -If you want more channels to be cachedm, set a larger value by setting the 'channelCacheSize' property. +If you want more channels to be cached, set a larger value by setting the 'channelCacheSize' property. In XML it would look like as follows: ==== From 547c140906e94dcd26ed3b8c01a6ab78d52960a8 Mon Sep 17 00:00:00 2001 From: Yann Bolliger Date: Tue, 5 Apr 2022 10:28:42 +0200 Subject: [PATCH 091/737] More formatting typos --- src/reference/asciidoc/amqp.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index e3c2b9beb0..fc6772a80d 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -514,7 +514,7 @@ You can use this exception in the `RetryPolicy` to recover the operation after s ===== Configuring the Underlying Client Connection Factory The `CachingConnectionFactory` uses an instance of the Rabbit client `ConnectionFactory`. -A number of configuration properties are passed through (`host, port, userName, password, requestedHeartBeat, and connectionTimeout` for example) when setting the equivalent property on the `CachingConnectionFactory`. +A number of configuration properties are passed through (`host`, `port`, `userName`, `password`, `requestedHeartBeat`, and `connectionTimeout` for example) when setting the equivalent property on the `CachingConnectionFactory`. To set other properties (`clientProperties`, for example), you can define an instance of the Rabbit factory and provide a reference to it by using the appropriate constructor of the `CachingConnectionFactory`. When using the namespace (<>), you need to provide a reference to the configured factory in the `connection-factory` attribute. For convenience, a factory bean is provided to assist in configuring the connection factory in a Spring application context, as discussed in <>. From 015b96943707e4dff17070b2172a2d64ae17a604 Mon Sep 17 00:00:00 2001 From: Jay <31400063+eeaters@users.noreply.github.com> Date: Mon, 11 Apr 2022 23:08:46 +0800 Subject: [PATCH 092/737] Fix AddressResolver typo in the amqp.adoc --- src/reference/asciidoc/amqp.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index fc6772a80d..3c20e12178 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -441,7 +441,7 @@ The following example with a custom thread factory that prefixes thread names wi ===== AddressResolver -Starting with version 2.1.15, you can now use an `AddressResover` to resolve the connection address(es). +Starting with version 2.1.15, you can now use an `AddressResolver` to resolve the connection address(es). This will override any settings of the `addresses` and `host/port` properties. ===== Naming Connections From 07a21d93cb9c18d9c1c6aecfbcaae76d8ada609b Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 12 Apr 2022 15:13:19 -0400 Subject: [PATCH 093/737] Remove micrometer-binders from build --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 8dfe94b081..ee0aeab410 100644 --- a/build.gradle +++ b/build.gradle @@ -386,7 +386,7 @@ project('spring-rabbit') { optionalApi 'io.projectreactor:reactor-core' optionalApi "ch.qos.logback:logback-classic:$logbackVersion" optionalApi 'org.apache.logging.log4j:log4j-core' - optionalApi 'io.micrometer:micrometer-binders' + optionalApi 'io.micrometer:micrometer-core' optionalApi 'io.micrometer:micrometer-tracing-api' // Spring Data projection message binding support optionalApi ("org.springframework.data:spring-data-commons") { From 0557be4e00130e7e8edc3a3910d0ff5b33d4804c Mon Sep 17 00:00:00 2001 From: maecval Date: Thu, 14 Apr 2022 17:37:08 +0200 Subject: [PATCH 094/737] =?UTF-8?q?Issue=201450:=20avoid=20NullPointerExce?= =?UTF-8?q?ption=20which=20occurs=20during=20shutdown=20o=E2=80=A6=20(#145?= =?UTF-8?q?1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Issue 1450: avoid NullPointerException which occurs during shutdown of the SpringBoot app * Update spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java Co-authored-by: Gary Russell Co-authored-by: Gary Russell --- .../amqp/rabbit/connection/AbstractConnectionFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java index db56a8d7bb..cc0d1c67be 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java @@ -212,7 +212,7 @@ protected ApplicationEventPublisher getApplicationEventPublisher() { @Override public void onApplicationEvent(ContextClosedEvent event) { - if (getApplicationContext().equals(event.getApplicationContext())) { + if (event.getApplicationContext().equals(getApplicationContext())) { this.contextStopped = true; } if (this.publisherConnectionFactory != null) { From 48c95d31aecf61f95df40f8cdc89097e6024ff0e Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 14 Apr 2022 14:18:08 -0400 Subject: [PATCH 095/737] Switch Micrometer to 1.10.0-SNAPSHOT --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ee0aeab410..1e7fecd5a4 100644 --- a/build.gradle +++ b/build.gradle @@ -55,7 +55,7 @@ ext { log4jVersion = '2.17.1' logbackVersion = '1.2.3' lz4Version = '1.8.0' - micrometerVersion = '2.0.0-SNAPSHOT' + micrometerVersion = '1.10.0-SNAPSHOT' micrometerTracingVersion = '1.0.0-SNAPSHOT' mockitoVersion = '4.0.0' rabbitmqStreamVersion = '0.4.0' From 3b4a7e494ea6cae7e7bf9381a477cd2d1c8ad1c6 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 20 Apr 2022 16:17:23 -0400 Subject: [PATCH 096/737] Fix Sonar Issue --- .../org/springframework/amqp/rabbit/core/RabbitTemplate.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java index bca2597b5b..f8ba1a1bb9 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java @@ -1079,7 +1079,7 @@ && isMandatoryFor(message), }, obtainTargetConnectionFactory(this.sendConnectionFactorySelectorExpression, message)); } - private ConnectionFactory obtainTargetConnectionFactory(Expression expression, Object rootObject) { + private ConnectionFactory obtainTargetConnectionFactory(Expression expression, @Nullable Object rootObject) { if (expression != null && getConnectionFactory() instanceof AbstractRoutingConnectionFactory) { AbstractRoutingConnectionFactory routingConnectionFactory = (AbstractRoutingConnectionFactory) getConnectionFactory(); From 59197ca12ac5abcaf3b93e985139c7676b7b87a5 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 20 Apr 2022 16:25:23 -0400 Subject: [PATCH 097/737] Fix Sonar Issue --- .../org/springframework/amqp/rabbit/core/RabbitTemplate.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java index f8ba1a1bb9..fe244fe596 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java @@ -1944,6 +1944,7 @@ protected Message doSendAndReceiveWithFixed(final String exchange, final String @Nullable private Message doSendAndReceiveWithDirect(String exchange, String routingKey, Message message, @Nullable CorrelationData correlationData) { + ConnectionFactory connectionFactory = obtainTargetConnectionFactory( this.sendConnectionFactorySelectorExpression, message); if (this.usePublisherConnection && connectionFactory.getPublisherConnectionFactory() != null) { @@ -1957,7 +1958,7 @@ private Message doSendAndReceiveWithDirect(String exchange, String routingKey, M boolean cancelConsumer = false; try { Channel channel = channelHolder.getChannel(); - if (isPublisherConfirmsOrReturns(connectionFactory)) { + if (isPublisherConfirmsOrReturns(connectionFactory)) { // NOSONAR false positive NP dereference addListener(channel); } Message reply = doSendAndReceiveAsListener(exchange, routingKey, message, correlationData, channel, From 9d49f20893c150a7b625d04e95dccdcde84a663a Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 20 Apr 2022 15:43:16 -0400 Subject: [PATCH 098/737] GH-1452: Close Connection in checkMissingQueues Resolves https://github.com/spring-projects/spring-amqp/issues/1452 While not a problem with connection factories provided by the framework (which all ignore `close()`) the code should properly close the connection after use. **cherry-pick to 2.4.x, 2.3.x** --- .../amqp/rabbit/listener/BlockingQueueConsumer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java index 263ac54604..d0f08d9222 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java @@ -554,7 +554,8 @@ private void checkMissingQueues() { Connection connection = null; // NOSONAR - RabbitUtils Channel channelForCheck = null; try { - channelForCheck = this.connectionFactory.createConnection().createChannel(false); + connection = this.connectionFactory.createConnection(); + channelForCheck = connection.createChannel(false); channelForCheck.queueDeclarePassive(queueToCheck); if (logger.isInfoEnabled()) { logger.info("Queue '" + queueToCheck + "' is now available"); From bf32231ad6cb28e249610873ed45dbfa4183f030 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 27 Apr 2022 10:47:27 -0400 Subject: [PATCH 099/737] GH-1455: AdviceChain on Stream Listener Container Resolves https://github.com/spring-projects/spring-amqp/issues/1455 Add an advice chain to the stream listener container and its factory. Add a `StreamMessageRecoverer` for native stream messages. Add a retry interceptor to work with native stream messages. **cherry-pick to 2.4.x** * Add since to new setter. --- .../StreamRabbitListenerContainerFactory.java | 12 +- .../listener/StreamListenerContainer.java | 53 +++++++-- .../adapter/StreamMessageListenerAdapter.java | 5 +- .../stream/retry/StreamMessageRecoverer.java | 48 ++++++++ ...RetryOperationsInterceptorFactoryBean.java | 79 +++++++++++++ .../rabbit/stream/retry/package-info.java | 4 + .../stream/listener/RabbitListenerTests.java | 86 ++++++++++---- .../StreamListenerContainerTests.java | 105 ++++++++++++++++++ ...bstractRabbitListenerContainerFactory.java | 25 +---- .../BaseRabbitListenerContainerFactory.java | 24 +++- ...RetryOperationsInterceptorFactoryBean.java | 34 +++--- .../amqp/rabbit/retry/MessageRecoverer.java | 5 +- src/reference/asciidoc/stream.adoc | 21 ++++ 13 files changed, 427 insertions(+), 74 deletions(-) create mode 100644 spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/StreamMessageRecoverer.java create mode 100644 spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/StreamRetryOperationsInterceptorFactoryBean.java create mode 100644 spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/package-info.java create mode 100644 spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/StreamListenerContainerTests.java diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/StreamRabbitListenerContainerFactory.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/StreamRabbitListenerContainerFactory.java index 7d24a61f32..0eb337abfd 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/StreamRabbitListenerContainerFactory.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/StreamRabbitListenerContainerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-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. @@ -18,12 +18,15 @@ import java.lang.reflect.Method; +import org.aopalliance.aop.Advice; + import org.springframework.amqp.rabbit.batch.BatchingStrategy; import org.springframework.amqp.rabbit.config.BaseRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.config.ContainerCustomizer; import org.springframework.amqp.rabbit.listener.MethodRabbitListenerEndpoint; import org.springframework.amqp.rabbit.listener.RabbitListenerEndpoint; import org.springframework.amqp.rabbit.listener.api.RabbitListenerErrorHandler; +import org.springframework.amqp.utils.JavaUtils; import org.springframework.lang.Nullable; import org.springframework.rabbit.stream.listener.ConsumerCustomizer; import org.springframework.rabbit.stream.listener.StreamListenerContainer; @@ -96,9 +99,10 @@ public StreamListenerContainer createListenerContainer(RabbitListenerEndpoint en }); } StreamListenerContainer container = createContainerInstance(); - if (this.consumerCustomizer != null) { - container.setConsumerCustomizer(this.consumerCustomizer); - } + Advice[] adviceChain = getAdviceChain(); + JavaUtils.INSTANCE + .acceptIfNotNull(this.consumerCustomizer, container::setConsumerCustomizer) + .acceptIfNotNull(adviceChain, container::setAdviceChain); applyCommonOverrides(endpoint, container); if (this.containerCustomizer != null) { this.containerCustomizer.configure(container); diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java index 4640defed7..67d51196a9 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-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. @@ -16,6 +16,7 @@ package org.springframework.rabbit.stream.listener; +import org.aopalliance.aop.Advice; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -23,6 +24,8 @@ import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.rabbit.listener.MessageListenerContainer; import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.beans.factory.BeanNameAware; import org.springframework.lang.Nullable; import org.springframework.rabbit.stream.support.StreamMessageProperties; @@ -62,6 +65,10 @@ public class StreamListenerContainer implements MessageListenerContainer, BeanNa private MessageListener messageListener; + private StreamMessageListener streamListener; + + private Advice[] adviceChain; + /** * Construct an instance using the provided environment. * @param environment the environment. @@ -154,6 +161,18 @@ public void setAutoStartup(boolean autoStart) { public boolean isAutoStartup() { return this.autoStartup; } + + /** + * Set an advice chain to apply to the listener. + * @param advices the advice chain. + * @since 2.4.5 + */ + public void setAdviceChain(Advice... advices) { + Assert.notNull(advices, "'advices' cannot be null"); + Assert.noNullElements(advices, "'advices' cannot have null elements"); + this.adviceChain = advices; + } + @Override @Nullable public Object getMessageListener() { @@ -183,26 +202,46 @@ public synchronized void stop() { @Override public void setupMessageListener(MessageListener messageListener) { - this.messageListener = messageListener; + adviseIfNeeded(messageListener); this.builder.messageHandler((context, message) -> { - if (messageListener instanceof StreamMessageListener) { - ((StreamMessageListener) messageListener).onStreamMessage(message, context); + if (this.streamListener != null) { + this.streamListener.onStreamMessage(message, context); } else { Message message2 = this.streamConverter.toMessage(message, new StreamMessageProperties(context)); - if (messageListener instanceof ChannelAwareMessageListener) { + if (this.messageListener instanceof ChannelAwareMessageListener) { try { - ((ChannelAwareMessageListener) messageListener).onMessage(message2, null); + ((ChannelAwareMessageListener) this.messageListener).onMessage(message2, null); } catch (Exception e) { // NOSONAR this.logger.error("Listner threw an exception", e); } } else { - messageListener.onMessage(message2); + this.messageListener.onMessage(message2); } } }); } + private void adviseIfNeeded(MessageListener messageListener) { + this.messageListener = messageListener; + if (messageListener instanceof StreamMessageListener) { + this.streamListener = (StreamMessageListener) messageListener; + } + if (this.adviceChain != null && this.adviceChain.length > 0) { + ProxyFactory factory = new ProxyFactory(messageListener); + for (Advice advice : this.adviceChain) { + factory.addAdvisor(new DefaultPointcutAdvisor(advice)); + } + factory.setInterfaces(messageListener.getClass().getInterfaces()); + if (this.streamListener != null) { + this.streamListener = (StreamMessageListener) factory.getProxy(getClass().getClassLoader()); + } + else { + this.messageListener = (MessageListener) factory.getProxy(getClass().getClassLoader()); + } + } + } + } diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/adapter/StreamMessageListenerAdapter.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/adapter/StreamMessageListenerAdapter.java index 3db6b3a3fe..359f3c6559 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/adapter/StreamMessageListenerAdapter.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/adapter/StreamMessageListenerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-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. @@ -21,6 +21,7 @@ import org.springframework.amqp.rabbit.listener.adapter.InvocationResult; import org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter; import org.springframework.amqp.rabbit.listener.api.RabbitListenerErrorHandler; +import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; import org.springframework.rabbit.stream.listener.StreamMessageListener; import com.rabbitmq.stream.Message; @@ -60,7 +61,7 @@ public void onStreamMessage(Message message, Context context) { } } catch (Exception ex) { - this.logger.error("Failed to invoke listener", ex); + throw new ListenerExecutionFailedException("Failed to invoke listener", ex); } } diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/StreamMessageRecoverer.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/StreamMessageRecoverer.java new file mode 100644 index 0000000000..222a5a215c --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/StreamMessageRecoverer.java @@ -0,0 +1,48 @@ +/* + * 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.rabbit.stream.retry; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.rabbit.retry.MessageRecoverer; + +import com.rabbitmq.stream.MessageHandler.Context; + +/** + * Implementations of this interface can handle failed messages after retries are + * exhausted. + * + * @author Gary Russell + * @since 2.4.5 + * + */ +@FunctionalInterface +public interface StreamMessageRecoverer extends MessageRecoverer { + + @Override + default void recover(Message message, Throwable cause) { + } + + /** + * Callback for message that was consumed but failed all retry attempts. + * + * @param message the message to recover. + * @param context the context. + * @param cause the cause of the error. + */ + void recover(com.rabbitmq.stream.Message message, Context context, Throwable cause); + +} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/StreamRetryOperationsInterceptorFactoryBean.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/StreamRetryOperationsInterceptorFactoryBean.java new file mode 100644 index 0000000000..e4960b4bb8 --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/StreamRetryOperationsInterceptorFactoryBean.java @@ -0,0 +1,79 @@ +/* + * 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.rabbit.stream.retry; + +import org.springframework.amqp.rabbit.config.StatelessRetryOperationsInterceptorFactoryBean; +import org.springframework.amqp.rabbit.retry.MessageRecoverer; +import org.springframework.rabbit.stream.listener.StreamListenerContainer; +import org.springframework.retry.RetryOperations; +import org.springframework.retry.interceptor.MethodInvocationRecoverer; +import org.springframework.retry.support.RetryTemplate; + +import com.rabbitmq.stream.Message; +import com.rabbitmq.stream.MessageHandler.Context; + +/** + * Convenient factory bean for creating a stateless retry interceptor for use in a + * {@link StreamListenerContainer} when consuming native stream messages, giving you a + * large amount of control over the behavior of a container when a listener fails. To + * control the number of retry attempt or the backoff in between attempts, supply a + * customized {@link RetryTemplate}. Stateless retry is appropriate if your listener can + * be called repeatedly between failures with no side effects. The semantics of stateless + * retry mean that a listener exception is not propagated to the container until the retry + * attempts are exhausted. When the retry attempts are exhausted it can be processed using + * a {@link StreamMessageRecoverer} if one is provided. + * + * @author Gary Russell + * + * @see RetryOperations#execute(org.springframework.retry.RetryCallback,org.springframework.retry.RecoveryCallback) + */ +public class StreamRetryOperationsInterceptorFactoryBean extends StatelessRetryOperationsInterceptorFactoryBean { + + @Override + protected MethodInvocationRecoverer createRecoverer() { + return (args, cause) -> { + StreamMessageRecoverer messageRecoverer = (StreamMessageRecoverer) getMessageRecoverer(); + Object arg = args[0]; + if (arg instanceof org.springframework.amqp.core.Message) { + return super.recover(args, cause); + } + else { + if (messageRecoverer == null) { + this.logger.warn("Message(s) dropped on recovery: " + arg, cause); + } + else { + messageRecoverer.recover((Message) arg, (Context) args[1], cause); + } + return null; + } + }; + } + + /** + * Set a {@link StreamMessageRecoverer} to call when retries are exhausted. + * @param messageRecoverer the recoverer. + */ + public void setStreamMessageRecoverer(StreamMessageRecoverer messageRecoverer) { + super.setMessageRecoverer(messageRecoverer); + } + + @Override + public void setMessageRecoverer(MessageRecoverer messageRecoverer) { + throw new UnsupportedOperationException("Use setStreamMessageRecoverer() instead"); + } + +} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/package-info.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/package-info.java new file mode 100644 index 0000000000..cabb5d622f --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides classes supporting retries. + */ +package org.springframework.rabbit.stream.retry; diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java index 2c7352cb89..b0f914d7ba 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-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. @@ -23,6 +23,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import org.junit.jupiter.api.Test; @@ -30,6 +31,7 @@ import org.springframework.amqp.core.QueueBuilder; import org.springframework.amqp.rabbit.annotation.EnableRabbit; import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.config.RetryInterceptorBuilder; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitAdmin; import org.springframework.amqp.rabbit.core.RabbitTemplate; @@ -38,9 +40,12 @@ import org.springframework.context.SmartLifecycle; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; import org.springframework.rabbit.stream.config.StreamRabbitListenerContainerFactory; import org.springframework.rabbit.stream.producer.RabbitStreamTemplate; +import org.springframework.rabbit.stream.retry.StreamRetryOperationsInterceptorFactoryBean; import org.springframework.rabbit.stream.support.StreamMessageProperties; +import org.springframework.retry.interceptor.RetryOperationsInterceptor; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; @@ -64,15 +69,6 @@ public class RabbitListenerTests extends AbstractIntegrationTests { @Autowired Config config; -// @AfterAll - causes test to throw errors - need to investigate - static void deleteQueues() { - try (Environment environment = Config.environment()) { - environment.deleteStream("test.stream.queue1"); - environment.deleteStream("test.stream.queue2"); - environment.deleteStream("stream.created.over.amqp"); - } - } - @Test void simple(@Autowired RabbitStreamTemplate template) throws Exception { Future future = template.convertAndSend("foo"); @@ -87,8 +83,8 @@ void simple(@Autowired RabbitStreamTemplate template) throws Exception { future = template.convertAndSend("bar", msg -> null); assertThat(future.get(10, TimeUnit.SECONDS)).isFalse(); assertThat(this.config.latch1.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(this.config.received).containsExactly("foo", "bar", "baz", "qux"); - assertThat(this.config.id).isEqualTo("test"); + assertThat(this.config.received).containsExactly("foo", "foo", "bar", "baz", "qux"); + assertThat(this.config.id).isEqualTo("testNative"); } @Test @@ -97,6 +93,8 @@ void nativeMsg(@Autowired RabbitTemplate template) throws InterruptedException { assertThat(this.config.latch2.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(this.config.receivedNative).isNotNull(); assertThat(this.config.context).isNotNull(); + assertThat(this.config.latch3.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(this.config.latch4.await(10, TimeUnit.SECONDS)).isTrue(); } @Test @@ -110,12 +108,18 @@ void queueOverAmqp() throws Exception { @EnableRabbit public static class Config { - final CountDownLatch latch1 = new CountDownLatch(4); + final CountDownLatch latch1 = new CountDownLatch(5); final CountDownLatch latch2 = new CountDownLatch(1); + final CountDownLatch latch3 = new CountDownLatch(3); + + final CountDownLatch latch4 = new CountDownLatch(1); + final List received = new ArrayList<>(); + final AtomicBoolean first = new AtomicBoolean(true); + volatile Message receivedNative; volatile Context context; @@ -133,12 +137,23 @@ static Environment environment() { SmartLifecycle creator(Environment env) { return new SmartLifecycle() { + boolean running; + @Override public void stop() { + clean(env); + this.running = false; } @Override public void start() { + clean(env); + env.streamCreator().stream("test.stream.queue1").create(); + env.streamCreator().stream("test.stream.queue2").create(); + this.running = true; + } + + private void clean(Environment env) { try { env.deleteStream("test.stream.queue1"); } @@ -149,42 +164,65 @@ public void start() { } catch (Exception e) { } - env.streamCreator().stream("test.stream.queue1").create(); - env.streamCreator().stream("test.stream.queue2").create(); + try { + env.deleteStream("stream.created.over.amqp"); + } + catch (Exception e) { + } } @Override public boolean isRunning() { - return false; + return this.running; } }; } @Bean RabbitListenerContainerFactory rabbitListenerContainerFactory(Environment env) { - return new StreamRabbitListenerContainerFactory(env); + StreamRabbitListenerContainerFactory factory = new StreamRabbitListenerContainerFactory(env); + factory.setAdviceChain(RetryInterceptorBuilder.stateless().build()); + return factory; } @RabbitListener(queues = "test.stream.queue1") void listen(String in) { this.received.add(in); this.latch1.countDown(); + if (first.getAndSet(false)) { + throw new RuntimeException("fail first"); + } } @Bean - RabbitListenerContainerFactory nativeFactory(Environment env) { + public StreamRetryOperationsInterceptorFactoryBean sfb() { + StreamRetryOperationsInterceptorFactoryBean rfb = new StreamRetryOperationsInterceptorFactoryBean(); + rfb.setStreamMessageRecoverer((msg, context, throwable) -> { + this.latch4.countDown(); + }); + return rfb; + } + + @Bean + @DependsOn("sfb") + RabbitListenerContainerFactory nativeFactory(Environment env, + RetryOperationsInterceptor retry) { + StreamRabbitListenerContainerFactory factory = new StreamRabbitListenerContainerFactory(env); factory.setNativeListener(true); factory.setConsumerCustomizer((id, builder) -> { - builder.name("myConsumer") + builder.name(id) .offset(OffsetSpecification.first()) .manualTrackingStrategy(); - this.id = id; + if (id.equals("testNative")) { + this.id = id; + } }); + factory.setAdviceChain(retry); return factory; } - @RabbitListener(id = "test", queues = "test.stream.queue2", containerFactory = "nativeFactory") + @RabbitListener(id = "testNative", queues = "test.stream.queue2", containerFactory = "nativeFactory") void nativeMsg(Message in, Context context) { this.receivedNative = in; this.context = context; @@ -192,6 +230,12 @@ void nativeMsg(Message in, Context context) { context.storeOffset(); } + @RabbitListener(id = "testNativeFail", queues = "test.stream.queue2", containerFactory = "nativeFactory") + void nativeMsgFail(Message in, Context context) { + this.latch3.countDown(); + throw new RuntimeException("fail all"); + } + @Bean CachingConnectionFactory cf() { return new CachingConnectionFactory("localhost", amqpPort()); diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/StreamListenerContainerTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/StreamListenerContainerTests.java new file mode 100644 index 0000000000..3c681f62cc --- /dev/null +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/StreamListenerContainerTests.java @@ -0,0 +1,105 @@ +/* + * 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.rabbit.stream.listener; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import org.aopalliance.intercept.MethodInterceptor; +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.core.MessageListener; +import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener; + +import com.rabbitmq.stream.ConsumerBuilder; +import com.rabbitmq.stream.Environment; +import com.rabbitmq.stream.Message; +import com.rabbitmq.stream.MessageHandler; +import com.rabbitmq.stream.MessageHandler.Context; + +/** + * @author Gary Russell + * @since 2.4.5 + * + */ +public class StreamListenerContainerTests { + + @Test + void testAdviceChain() throws Exception { + Environment env = mock(Environment.class); + ConsumerBuilder builder = mock(ConsumerBuilder.class); + given(env.consumerBuilder()).willReturn(builder); + AtomicReference handler = new AtomicReference<>(); + willAnswer(inv -> { + handler.set(inv.getArgument(0)); + return null; + } + ).given(builder).messageHandler(any()); + AtomicBoolean advised = new AtomicBoolean(); + MethodInterceptor advice = (inv) -> { + advised.set(true); + return inv.proceed(); + }; + + StreamListenerContainer container = new StreamListenerContainer(env); + container.setAdviceChain(advice); + AtomicBoolean called = new AtomicBoolean(); + MessageListener ml = mock(MessageListener.class); + willAnswer(inv -> { + called.set(true); + return null; + }).given(ml).onMessage(any()); + container.setupMessageListener(ml); + Message message = mock(Message.class); + given(message.getBodyAsBinary()).willReturn("foo".getBytes()); + Context context = mock(Context.class); + handler.get().handle(context, message); + assertThat(advised.get()).isTrue(); + assertThat(called.get()).isTrue(); + + advised.set(false); + called.set(false); + ChannelAwareMessageListener cal = mock(ChannelAwareMessageListener.class); + willAnswer(inv -> { + called.set(true); + return null; + }).given(cal).onMessage(any(), isNull()); + container.setupMessageListener(cal); + handler.get().handle(context, message); + assertThat(advised.get()).isTrue(); + assertThat(called.get()).isTrue(); + + called.set(false); + StreamMessageListener sml = mock(StreamMessageListener.class); + willAnswer(inv -> { + called.set(true); + return null; + }).given(sml).onStreamMessage(message, context); + container.setupMessageListener(sml); + handler.get().handle(context, message); + assertThat(advised.get()).isTrue(); + assertThat(called.get()).isTrue(); + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java index d94ad2a2dc..193ea80de8 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 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. @@ -39,7 +39,6 @@ import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; -import org.springframework.lang.Nullable; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.util.Assert; import org.springframework.util.ErrorHandler; @@ -86,8 +85,6 @@ public abstract class AbstractRabbitListenerContainerFactory recoveryCallback; + private Advice[] adviceChain; + @Override public abstract C createListenerContainer(RabbitListenerEndpoint endpoint); @@ -129,4 +134,21 @@ protected void applyCommonOverrides(@Nullable RabbitListenerEndpoint endpoint, C } } + /** + * @return the advice chain that was set. Defaults to {@code null}. + * @since 1.7.4 + */ + @Nullable + public Advice[] getAdviceChain() { + return this.adviceChain == null ? null : Arrays.copyOf(this.adviceChain, this.adviceChain.length); + } + + /** + * @param adviceChain the advice chain to set. + * @see AbstractMessageListenerContainer#setAdviceChain + */ + public void setAdviceChain(Advice... adviceChain) { + this.adviceChain = adviceChain == null ? null : Arrays.copyOf(adviceChain, adviceChain.length); + } + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatelessRetryOperationsInterceptorFactoryBean.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatelessRetryOperationsInterceptorFactoryBean.java index eb6c6b6332..4530e6f9c6 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatelessRetryOperationsInterceptorFactoryBean.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatelessRetryOperationsInterceptorFactoryBean.java @@ -46,7 +46,7 @@ */ public class StatelessRetryOperationsInterceptorFactoryBean extends AbstractRetryOperationsInterceptorFactoryBean { - private static Log logger = LogFactory.getLog(StatelessRetryOperationsInterceptorFactoryBean.class); + protected final Log logger = LogFactory.getLog(getClass()); // NOSONAR @Override public RetryOperationsInterceptor getObject() { @@ -63,21 +63,23 @@ public RetryOperationsInterceptor getObject() { } @SuppressWarnings("unchecked") - private MethodInvocationRecoverer createRecoverer() { - return (args, cause) -> { - MessageRecoverer messageRecoverer = getMessageRecoverer(); - Object arg = args[1]; - if (messageRecoverer == null) { - logger.warn("Message(s) dropped on recovery: " + arg, cause); - } - else if (arg instanceof Message) { - messageRecoverer.recover((Message) arg, cause); - } - else if (arg instanceof List && messageRecoverer instanceof MessageBatchRecoverer) { - ((MessageBatchRecoverer) messageRecoverer).recover((List) arg, cause); - } - return null; - }; + protected MethodInvocationRecoverer createRecoverer() { + return this::recover; + } + + protected Object recover(Object[] args, Throwable cause) { + MessageRecoverer messageRecoverer = getMessageRecoverer(); + Object arg = args[1]; + if (messageRecoverer == null) { + this.logger.warn("Message(s) dropped on recovery: " + arg, cause); + } + else if (arg instanceof Message) { + messageRecoverer.recover((Message) arg, cause); + } + else if (arg instanceof List && messageRecoverer instanceof MessageBatchRecoverer) { + ((MessageBatchRecoverer) messageRecoverer).recover((List) arg, cause); + } + return null; } @Override diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/MessageRecoverer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/MessageRecoverer.java index 9c547b9696..61859705df 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/MessageRecoverer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/MessageRecoverer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -19,6 +19,9 @@ import org.springframework.amqp.core.Message; /** + * Implementations of this interface can handle failed messages after retries are + * exhausted. + * * @author Dave Syer * @author Gary Russell * diff --git a/src/reference/asciidoc/stream.adoc b/src/reference/asciidoc/stream.adoc index 11f836d8a0..5e24395bee 100644 --- a/src/reference/asciidoc/stream.adoc +++ b/src/reference/asciidoc/stream.adoc @@ -136,3 +136,24 @@ void nativeMsg(Message in, Context context) { } ---- ==== + +Version 2.4.5 added the `adviceChain` property to the `StreamListenerContainer` (and its factory). +A new factory bean is also provided to create a stateless retry interceptor with an optional `StreamMessageRecoverer` for use when consuming raw stream messages. + +==== +[source, java] +---- +@Bean +public StreamRetryOperationsInterceptorFactoryBean sfb(RetryTemplate retryTemplate) { + StreamRetryOperationsInterceptorFactoryBean rfb = + new StreamRetryOperationsInterceptorFactoryBean(); + rfb.setRetryOperations(retryTemplate); + rfb.setStreamMessageRecoverer((msg, context, throwable) -> { + ... + }); + return rfb; +} +---- +==== + +IMPORTANT: Stateful retry is not supported with this container. From 1ebde8eceb0180f9cce48b98829e222c6d997f03 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 16 May 2022 10:37:47 -0400 Subject: [PATCH 100/737] Upgrade Versions; Prepare for Milestone Release --- build.gradle | 22 +++++++++---------- .../EnableRabbitIntegrationTests.java | 3 +-- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/build.gradle b/build.gradle index 1e7fecd5a4..2ce43489c6 100644 --- a/build.gradle +++ b/build.gradle @@ -39,33 +39,33 @@ ext { modifiedFiles = files(grgit.status().unstaged.modified).filter{ f -> f.name.endsWith('.java') || f.name.endsWith('.kt') } - assertjVersion = '3.21.0' + assertjVersion = '3.22.0' assertkVersion = '0.24' - awaitilityVersion = '4.1.1' + awaitilityVersion = '4.2.0' commonsCompressVersion = '1.20' commonsHttpClientVersion = '4.5.13' commonsPoolVersion = '2.11.1' googleJsr305Version = '3.0.2' hamcrestVersion = '2.2' - hibernateValidationVersion = '6.2.3.Final' - jacksonBomVersion = '2.13.2.20220328' + hibernateValidationVersion = '7.0.4.Final' + jacksonBomVersion = '2.13.3' jaywayJsonPathVersion = '2.6.0' junit4Version = '4.13.2' junitJupiterVersion = '5.8.2' - log4jVersion = '2.17.1' + log4jVersion = '2.17.2' logbackVersion = '1.2.3' lz4Version = '1.8.0' - micrometerVersion = '1.10.0-SNAPSHOT' - micrometerTracingVersion = '1.0.0-SNAPSHOT' - mockitoVersion = '4.0.0' + micrometerVersion = '1.10.0-M2' + micrometerTracingVersion = '1.0.0-M5' + mockitoVersion = '4.5.1' rabbitmqStreamVersion = '0.4.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.13.1' rabbitmqHttpClientVersion = '3.12.1' - reactorVersion = '2020.0.17' + reactorVersion = '2020.0.18' snappyVersion = '1.1.8.4' springDataVersion = '2022.0.0-M3' - springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.0-M3' - springRetryVersion = '1.3.2' + springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.0-M4' + springRetryVersion = '1.3.3' zstdJniVersion = '1.5.0-2' } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java index 3b2bee26b4..84196b3697 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java @@ -42,8 +42,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import javax.validation.Valid; - import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; @@ -149,6 +147,7 @@ import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import jakarta.validation.Valid; /** * From 58d4e72d92f570f1e80e75d9e15a63c6e6a52e68 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 16 May 2022 11:03:40 -0400 Subject: [PATCH 101/737] Work Around HOP/REST Incompatibility in Test --- ...StatelessRetryOperationsInterceptorFactoryBean.java | 1 + .../amqp/rabbit/core/RabbitRestApiTests.java | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatelessRetryOperationsInterceptorFactoryBean.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatelessRetryOperationsInterceptorFactoryBean.java index 4530e6f9c6..bb69a0b374 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatelessRetryOperationsInterceptorFactoryBean.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatelessRetryOperationsInterceptorFactoryBean.java @@ -67,6 +67,7 @@ protected MethodInvocationRecoverer createRecoverer() { return this::recover; } + @SuppressWarnings("unchecked") protected Object recover(Object[] args, Throwable cause) { MessageRecoverer messageRecoverer = getMessageRecoverer(); Object arg = args[1]; diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitRestApiTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitRestApiTests.java index 4a574ad87f..04cbf541d2 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitRestApiTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitRestApiTests.java @@ -17,8 +17,10 @@ package org.springframework.amqp.rabbit.core; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.awaitility.Awaitility.await; +import java.io.IOException; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.util.Collections; @@ -29,6 +31,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.springframework.amqp.AmqpException; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.DirectExchange; @@ -217,7 +220,12 @@ public void testDeleteExchange() { assertThat(exchangeToAssert.getName()).isEqualTo(testExchange.getName()); assertThat(exchangeToAssert.getType()).isEqualTo(testExchange.getType()); this.rabbitRestClient.deleteExchange("/", testExchange.getName()); - assertThat(this.rabbitRestClient.getExchange("/", exchangeName)).isNull(); + // 6.0.0 REST compatibility +// assertThat(this.rabbitRestClient.getExchange("/", exchangeName)).isNull(); + RabbitTemplate template = new RabbitTemplate(this.connectionFactory); + assertThatExceptionOfType(AmqpException.class) + .isThrownBy(() -> template.execute(channel -> channel.exchangeDeclarePassive(exchangeName))) + .withCauseExactlyInstanceOf(IOException.class); } } From 398294431eb42908415b078b5dc6dcfe689fcf66 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 16 May 2022 15:28:25 +0000 Subject: [PATCH 102/737] [artifactory-release] Release version 3.0.0-M3 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 2477c6a3c5..bd03825f33 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.0-SNAPSHOT +version=3.0.0-M3 org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true From fe37e0159782198c6583c3d77cf696a32bdb9f44 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 16 May 2022 15:28:27 +0000 Subject: [PATCH 103/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index bd03825f33..2477c6a3c5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.0-M3 +version=3.0.0-SNAPSHOT org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true From cffebb9d27ebddf39996302e5b68649a105ca4b4 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 18 May 2022 15:39:40 -0400 Subject: [PATCH 104/737] GH-1459: Improve MeterRegistry Discovery Resolves https://github.com/spring-projects/spring-amqp/issues/1459 * Add micrometer properties and container customizers to LCFB. * Use `ObjectProvider` to locate registry. --- .../config/ListenerContainerFactoryBean.java | 58 ++++++- .../AbstractMessageListenerContainer.java | 74 --------- .../rabbit/listener/MicrometerHolder.java | 146 ++++++++++++++++++ .../ListenerContainerFactoryBeanTests.java | 77 +++++++++ .../listener/MicrometerHolderTests.java | 118 ++++++++++++++ src/reference/asciidoc/amqp.adoc | 2 +- 6 files changed, 399 insertions(+), 76 deletions(-) create mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MicrometerHolder.java create mode 100644 spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBeanTests.java create mode 100644 spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MicrometerHolderTests.java diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java index 07c7be9f41..c597f3e887 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java @@ -16,6 +16,7 @@ package org.springframework.amqp.rabbit.config; +import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; @@ -61,6 +62,8 @@ public class ListenerContainerFactoryBean extends AbstractFactoryBean implements ApplicationContextAware, BeanNameAware, ApplicationEventPublisherAware, SmartLifecycle { + private final Map micrometerTags = new HashMap<>(); + private ApplicationContext applicationContext; private String beanName; @@ -171,6 +174,12 @@ public class ListenerContainerFactoryBean extends AbstractFactoryBean smlcCustomizer; + + private ContainerCustomizer dmlcCustomizer; + @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; @@ -427,6 +436,44 @@ public void setRetryDeclarationInterval(long retryDeclarationInterval) { this.retryDeclarationInterval = retryDeclarationInterval; } + /** + * Set to false to disable micrometer listener timers. + * @param micrometerEnabled false to disable. + * @since 2.4.6 + */ + public void setMicrometerEnabled(boolean enabled) { + this.micrometerEnabled = enabled; + } + + /** + * Set additional tags for the Micrometer listener timers. + * @param tags the tags. + * @since 2.4.6 + */ + public void setMicrometerTags(Map tags) { + this.micrometerTags.putAll(tags); + } + + /** + * Set a {@link ContainerCustomizer} that is invoked after a container is created and + * configured to enable further customization of the container. + * @param containerCustomizer the customizer. + * @since 2.4.6 + */ + public void setSMLCCustomizer(ContainerCustomizer customizer) { + this.smlcCustomizer = customizer; + } + + /** + * Set a {@link ContainerCustomizer} that is invoked after a container is created and + * configured to enable further customization of the container. + * @param containerCustomizer the customizer. + * @since 2.4.6 + */ + public void setDMLCCustomizer(ContainerCustomizer customizer) { + this.dmlcCustomizer = customizer; + } + @Override public Class getObjectType() { return this.listenerContainer == null @@ -478,7 +525,16 @@ protected AbstractMessageListenerContainer createInstance() { // NOSONAR complex .acceptIfNotNull(this.autoDeclare, container::setAutoDeclare) .acceptIfNotNull(this.failedDeclarationRetryInterval, container::setFailedDeclarationRetryInterval) .acceptIfNotNull(this.exclusiveConsumerExceptionLogger, - container::setExclusiveConsumerExceptionLogger); + container::setExclusiveConsumerExceptionLogger) + .acceptIfNotNull(this.micrometerEnabled, container::setMicrometerEnabled) + .acceptIfCondition(this.micrometerTags.size() > 0, this.micrometerTags, + container::setMicrometerTags); + if (this.smlcCustomizer != null && this.type.equals(Type.simple)) { + this.smlcCustomizer.configure((SimpleMessageListenerContainer) container); + } + else if (this.dmlcCustomizer != null && this.type.equals(Type.direct)) { + this.dmlcCustomizer.configure((DirectMessageListenerContainer) container); + } container.afterPropertiesSet(); this.listenerContainer = container; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index 17b2beb1c6..9f77462b76 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -26,8 +26,6 @@ import java.util.Map.Entry; import java.util.Properties; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; import java.util.stream.Collectors; @@ -93,10 +91,6 @@ import com.rabbitmq.client.Channel; import com.rabbitmq.client.ShutdownSignalException; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Timer; -import io.micrometer.core.instrument.Timer.Builder; -import io.micrometer.core.instrument.Timer.Sample; /** * @author Mark Pollack @@ -2102,72 +2096,4 @@ else if (!RabbitUtils.isNormalChannelClose(cause)) { } - private static final class MicrometerHolder { - - private final ConcurrentMap timers = new ConcurrentHashMap<>(); - - private final MeterRegistry registry; - - private final Map tags; - - private final String listenerId; - - MicrometerHolder(@Nullable ApplicationContext context, String listenerId, Map tags) { - if (context == null) { - throw new IllegalStateException("No micrometer registry present"); - } - Map registries = context.getBeansOfType(MeterRegistry.class, false, false); - if (registries.size() == 1) { - this.registry = registries.values().iterator().next(); - this.listenerId = listenerId; - this.tags = tags; - } - else { - throw new IllegalStateException("No micrometer registry present"); - } - } - - Object start() { - return Timer.start(this.registry); - } - - void success(Object sample, String queue) { - Timer timer = this.timers.get(queue + "none"); - if (timer == null) { - timer = buildTimer(this.listenerId, "success", queue, "none"); - } - ((Sample) sample).stop(timer); - } - - void failure(Object sample, String queue, String exception) { - Timer timer = this.timers.get(queue + exception); - if (timer == null) { - timer = buildTimer(this.listenerId, "failure", queue, exception); - } - ((Sample) sample).stop(timer); - } - - private Timer buildTimer(String aListenerId, String result, String queue, String exception) { - - Builder builder = Timer.builder("spring.rabbitmq.listener") - .description("Spring RabbitMQ Listener") - .tag("listener.id", aListenerId) - .tag("queue", queue) - .tag("result", result) - .tag("exception", exception); - if (this.tags != null && !this.tags.isEmpty()) { - this.tags.forEach((key, value) -> builder.tag(key, value)); - } - Timer registeredTimer = builder.register(this.registry); - this.timers.put(queue + exception, registeredTimer); - return registeredTimer; - } - - void destroy() { - this.timers.values().forEach(this.registry::remove); - this.timers.clear(); - } - - } - } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MicrometerHolder.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MicrometerHolder.java new file mode 100644 index 0000000000..fb00ae38d0 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MicrometerHolder.java @@ -0,0 +1,146 @@ +/* + * 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; + +import java.util.Collections; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.lang.Nullable; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.Timer.Builder; +import io.micrometer.core.instrument.Timer.Sample; + +/** + * Abstraction to avoid hard reference to Micrometer. + * + * @author Gary Russell + * @since 2.4.6 + * + */ +final class MicrometerHolder { + + private final ConcurrentMap timers = new ConcurrentHashMap<>(); + + private final MeterRegistry registry; + + private final Map tags; + + private final String listenerId; + + MicrometerHolder(@Nullable ApplicationContext context, String listenerId, Map tags) { + if (context == null) { + throw new IllegalStateException("No micrometer registry present"); + } + try { + this.registry = context.getBeanProvider(MeterRegistry.class).getIfUnique(); + } + catch (NoUniqueBeanDefinitionException ex) { + throw new IllegalStateException(ex); + } + if (this.registry != null) { + this.listenerId = listenerId; + this.tags = tags; + } + else { + throw new IllegalStateException("No micrometer registry present (or more than one and " + + "there is not exactly one marked with @Primary)"); + } + } + + private Map filterRegistries(Map registries, + ApplicationContext context) { + + if (registries.size() == 1) { + return registries; + } + MeterRegistry primary = null; + if (context instanceof ConfigurableApplicationContext) { + BeanDefinitionRegistry bdr = (BeanDefinitionRegistry) ((ConfigurableApplicationContext) context) + .getBeanFactory(); + for (Entry entry : registries.entrySet()) { + BeanDefinition beanDefinition = bdr.getBeanDefinition(entry.getKey()); + if (beanDefinition.isPrimary()) { + if (primary != null) { + primary = null; + break; + } + else { + primary = entry.getValue(); + } + } + } + } + if (primary != null) { + return Collections.singletonMap("primary", primary); + } + else { + return registries; + } + } + + Object start() { + return Timer.start(this.registry); + } + + void success(Object sample, String queue) { + Timer timer = this.timers.get(queue + "none"); + if (timer == null) { + timer = buildTimer(this.listenerId, "success", queue, "none"); + } + ((Sample) sample).stop(timer); + } + + void failure(Object sample, String queue, String exception) { + Timer timer = this.timers.get(queue + exception); + if (timer == null) { + timer = buildTimer(this.listenerId, "failure", queue, exception); + } + ((Sample) sample).stop(timer); + } + + private Timer buildTimer(String aListenerId, String result, String queue, String exception) { + + Builder builder = Timer.builder("spring.rabbitmq.listener") + .description("Spring RabbitMQ Listener") + .tag("listener.id", aListenerId) + .tag("queue", queue) + .tag("result", result) + .tag("exception", exception); + if (this.tags != null && !this.tags.isEmpty()) { + this.tags.forEach((key, value) -> builder.tag(key, value)); + } + Timer registeredTimer = builder.register(this.registry); + this.timers.put(queue + exception, registeredTimer); + return registeredTimer; + } + + void destroy() { + this.timers.values().forEach(this.registry::remove); + this.timers.clear(); + } + +} diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBeanTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBeanTests.java new file mode 100644 index 0000000000..3b9602616c --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBeanTests.java @@ -0,0 +1,77 @@ +/* + * 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.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.rabbit.config.ListenerContainerFactoryBean.Type; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; +import org.springframework.amqp.utils.test.TestUtils; + +/** + * @author Gary Russell + * @since 2.4.6 + * + */ +public class ListenerContainerFactoryBeanTests { + + @SuppressWarnings("unchecked") + @Test + void micrometer() throws Exception { + ListenerContainerFactoryBean lcfb = new ListenerContainerFactoryBean(); + lcfb.setConnectionFactory(mock(ConnectionFactory.class)); + lcfb.setMicrometerEnabled(false); + lcfb.setMicrometerTags(Map.of("foo", "bar")); + lcfb.afterPropertiesSet(); + AbstractMessageListenerContainer container = lcfb.getObject(); + assertThat(TestUtils.getPropertyValue(container, "micrometerEnabled", Boolean.class)).isFalse(); + assertThat(TestUtils.getPropertyValue(container, "micrometerTags", Map.class)).hasSize(1); + } + + @Test + void smlcCustomizer() throws Exception { + ListenerContainerFactoryBean lcfb = new ListenerContainerFactoryBean(); + lcfb.setConnectionFactory(mock(ConnectionFactory.class)); + lcfb.setSMLCCustomizer(container -> { + container.setConsumerStartTimeout(42L); + }); + lcfb.afterPropertiesSet(); + AbstractMessageListenerContainer container = lcfb.getObject(); + assertThat(TestUtils.getPropertyValue(container, "consumerStartTimeout", Long.class)).isEqualTo(42L); + } + + @Test + void dmlcCustomizer() throws Exception { + ListenerContainerFactoryBean lcfb = new ListenerContainerFactoryBean(); + lcfb.setConnectionFactory(mock(ConnectionFactory.class)); + lcfb.setType(Type.direct); + lcfb.setDMLCCustomizer(container -> { + container.setConsumersPerQueue(2); + }); + lcfb.afterPropertiesSet(); + AbstractMessageListenerContainer container = lcfb.getObject(); + assertThat(TestUtils.getPropertyValue(container, "consumersPerQueue", Integer.class)).isEqualTo(2); + } + + +} diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MicrometerHolderTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MicrometerHolderTests.java new file mode 100644 index 0000000000..b576e507f3 --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MicrometerHolderTests.java @@ -0,0 +1,118 @@ +/* + * 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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.Mockito.mock; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.test.util.ReflectionTestUtils; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; + +/** + * @author Gary Russell + */ +public class MicrometerHolderTests { + + @Test + void multiReg() { + ApplicationContext context = new AnnotationConfigApplicationContext(Config1.class); + assertThatIllegalStateException().isThrownBy(() -> new MicrometerHolder(context, "", Collections.emptyMap())) + .withMessage("No micrometer registry present (or more than one and " + + "there is not exactly one marked with @Primary)"); + } + + @Test + void twoPrimaries() { + ApplicationContext context = new AnnotationConfigApplicationContext(Config2.class); + assertThatIllegalStateException().isThrownBy(() -> new MicrometerHolder(context, "", Collections.emptyMap())) + .withMessageContaining("more than one 'primary' bean"); + } + + @Test + void primary() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config3.class); + MicrometerHolder micrometerHolder = new MicrometerHolder(ctx, "holderName", Collections.emptyMap()); + Timer.Sample sample = mock(Timer.Sample.class); + micrometerHolder.success(sample, "queue"); + micrometerHolder.failure(sample, "queue", "SomeException"); + @SuppressWarnings("unchecked") + Map meters = (Map) ReflectionTestUtils.getField(micrometerHolder, "timers"); + assertThat(meters).hasSize(2); + ctx.close(); + micrometerHolder.destroy(); + assertThat(meters).hasSize(0); + } + + static class Config1 { + + @Bean + MeterRegistry reg1() { + return new SimpleMeterRegistry(); + } + + @Bean + MeterRegistry reg2() { + return new SimpleMeterRegistry(); + } + + } + + static class Config2 { + + @Bean + @Primary + MeterRegistry reg1() { + return new SimpleMeterRegistry(); + } + + @Bean + @Primary + MeterRegistry reg2() { + return new SimpleMeterRegistry(); + } + + } + + static class Config3 { + + @Bean + @Primary + MeterRegistry reg1() { + return new SimpleMeterRegistry(); + } + + @Bean + MeterRegistry reg2() { + return new SimpleMeterRegistry(); + } + + } + +} diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index 3c20e12178..a0931518fb 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -3746,7 +3746,7 @@ Instead, you should hand off the event to a different thread that can then stop [[micrometer]] ===== Monitoring Listener Performance -Starting with version 2.2, the listener containers will automatically create and update Micrometer `Timer` s for the listener, if `Micrometer` is detected on the class path, and a `MeterRegistry` is present in the application context. +Starting with version 2.2, the listener containers will automatically create and update Micrometer `Timer` s for the listener, if `Micrometer` is detected on the class path, and a single `MeterRegistry` is present in the application context (or exactly one is annotated `@Primary`, such as when using Spring Boot). The timers can be disabled by setting the container property `micrometerEnabled` to `false`. Two timers are maintained - one for successful calls to the listener and one for failures. From 9d8dfe88abf9106bb0a8b9c34129c27cca143855 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 18 May 2022 15:59:33 -0400 Subject: [PATCH 105/737] GH-1459: Remove Unused Method --- .../rabbit/listener/MicrometerHolder.java | 36 ------------------- 1 file changed, 36 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MicrometerHolder.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MicrometerHolder.java index fb00ae38d0..694dabe6e4 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MicrometerHolder.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MicrometerHolder.java @@ -16,17 +16,12 @@ package org.springframework.amqp.rabbit.listener; -import java.util.Collections; import java.util.Map; -import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.context.ApplicationContext; -import org.springframework.context.ConfigurableApplicationContext; import org.springframework.lang.Nullable; import io.micrometer.core.instrument.MeterRegistry; @@ -71,37 +66,6 @@ final class MicrometerHolder { } } - private Map filterRegistries(Map registries, - ApplicationContext context) { - - if (registries.size() == 1) { - return registries; - } - MeterRegistry primary = null; - if (context instanceof ConfigurableApplicationContext) { - BeanDefinitionRegistry bdr = (BeanDefinitionRegistry) ((ConfigurableApplicationContext) context) - .getBeanFactory(); - for (Entry entry : registries.entrySet()) { - BeanDefinition beanDefinition = bdr.getBeanDefinition(entry.getKey()); - if (beanDefinition.isPrimary()) { - if (primary != null) { - primary = null; - break; - } - else { - primary = entry.getValue(); - } - } - } - } - if (primary != null) { - return Collections.singletonMap("primary", primary); - } - else { - return registries; - } - } - Object start() { return Timer.start(this.registry); } From f63c1ed1228028ec46694c1f92629d19e6216b2e Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 18 May 2022 16:05:14 -0400 Subject: [PATCH 106/737] GH-1459: Fix Javadocs --- .../amqp/rabbit/config/ListenerContainerFactoryBean.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java index c597f3e887..bbe884e25a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 the original author or authors. + * Copyright 2016-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. @@ -438,7 +438,7 @@ public void setRetryDeclarationInterval(long retryDeclarationInterval) { /** * Set to false to disable micrometer listener timers. - * @param micrometerEnabled false to disable. + * @param enabled false to disable. * @since 2.4.6 */ public void setMicrometerEnabled(boolean enabled) { @@ -457,7 +457,7 @@ public void setMicrometerTags(Map tags) { /** * Set a {@link ContainerCustomizer} that is invoked after a container is created and * configured to enable further customization of the container. - * @param containerCustomizer the customizer. + * @param customizer the customizer. * @since 2.4.6 */ public void setSMLCCustomizer(ContainerCustomizer customizer) { @@ -467,7 +467,7 @@ public void setSMLCCustomizer(ContainerCustomizer customizer) { From 6c7d53cb11203409533c9b8f2e47f5e3fbbc3311 Mon Sep 17 00:00:00 2001 From: Alexey Anufriev Date: Mon, 23 May 2022 17:20:50 +0200 Subject: [PATCH 107/737] Remove extra space in SimpleConnection.toString() --- .../amqp/rabbit/connection/SimpleConnection.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleConnection.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleConnection.java index d627c333f4..c59aadc2a1 100755 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleConnection.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -152,7 +152,7 @@ public com.rabbitmq.client.Connection getDelegate() { public String toString() { return "SimpleConnection@" + ObjectUtils.getIdentityHexString(this) - + " [delegate=" + this.delegate + ", localPort= " + getLocalPort() + "]"; + + " [delegate=" + this.delegate + ", localPort=" + getLocalPort() + "]"; } } From bd46d4709114ecb41644800227ae25e84af09e45 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 15 Jun 2022 10:07:29 -0400 Subject: [PATCH 108/737] GH-1463: RabbitTemplate.logReceived() Protected Resolves https://github.com/spring-projects/spring-amqp/issues/1463 **cherry-pick to 2.4.x** --- .../amqp/rabbit/core/RabbitTemplate.java | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java index fe244fe596..3160309366 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java @@ -1222,7 +1222,7 @@ else if (isChannelTransacted()) { } return null; }, obtainTargetConnectionFactory(this.receiveConnectionFactorySelectorExpression, queueName)); - logReceived(message); + logReceived("Received: ", message); return message; } @@ -1261,7 +1261,7 @@ else if (isChannelTransacted()) { return buildMessageFromDelivery(delivery); } }, obtainTargetConnectionFactory(this.receiveConnectionFactorySelectorExpression, null)); - logReceived(message); + logReceived("Received: ", message); return message; } @@ -1412,7 +1412,7 @@ else if (channelTransacted) { receiveMessage = buildMessageFromDelivery(delivery); } } - logReceived(receiveMessage); + logReceived("Received: ", receiveMessage); return receiveMessage; } @@ -1467,12 +1467,21 @@ private Delivery consumeDelivery(Channel channel, String queueName, long timeout return delivery; } - private void logReceived(@Nullable Message message) { - if (message == null) { - logger.debug("Received no message"); - } - else if (logger.isDebugEnabled()) { - logger.debug("Received: " + message); + /** + * Log a received message. The default implementation logs the full message at DEBUG + * level. Override this method to change that behavior. + * @param prefix a prefix, e.g. "Received: " or "Reply: ". + * @param message the message. + * @since 2.4.6 + */ + protected void logReceived(String prefix, @Nullable Message message) { + if (logger.isDebugEnabled()) { + if (message == null) { + logger.debug(prefix + "no message"); + } + else { + logger.debug(prefix + message); + } } } @@ -2091,9 +2100,7 @@ private Message exchangeMessages(final String exchange, final String routingKey, } doSend(channel, exchange, routingKey, message, mandatory, correlationData); reply = this.replyTimeout < 0 ? pendingReply.get() : pendingReply.get(this.replyTimeout, TimeUnit.MILLISECONDS); - if (this.logger.isDebugEnabled()) { - this.logger.debug("Reply: " + reply); - } + logReceived("Reply: ", reply); if (reply == null) { replyTimedOut(message.getMessageProperties().getCorrelationId()); } From 76348cb584f0449448995e9b01777f9f748404ba Mon Sep 17 00:00:00 2001 From: zysaaa <982020642@qq.com> Date: Fri, 17 Dec 2021 00:10:06 +0800 Subject: [PATCH 109/737] GH-1338: Add MessageAckListener Resolves https://github.com/spring-projects/spring-amqp/issues/1338 GH-1338: Test case for MessageAckListener --- ...bstractRabbitListenerContainerFactory.java | 15 ++- .../AbstractMessageListenerContainer.java | 17 +++ .../listener/BlockingQueueConsumer.java | 36 +++++- .../DirectMessageListenerContainer.java | 54 ++++++-- .../rabbit/listener/MessageAckListener.java | 41 +++++++ .../SimpleMessageListenerContainer.java | 2 +- ...sageListenerContainerIntegrationTests.java | 116 ++++++++++++++++++ ...ageListenerContainerIntegration2Tests.java | 72 +++++++++++ 8 files changed, 341 insertions(+), 12 deletions(-) create mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MessageAckListener.java diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java index 193ea80de8..520a3752af 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java @@ -30,6 +30,7 @@ import org.springframework.amqp.rabbit.batch.BatchingStrategy; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; +import org.springframework.amqp.rabbit.listener.MessageAckListener; import org.springframework.amqp.rabbit.listener.RabbitListenerEndpoint; import org.springframework.amqp.support.ConsumerTagStrategy; import org.springframework.amqp.support.converter.MessageConverter; @@ -115,6 +116,8 @@ public abstract class AbstractRabbitListenerContainerFactory(this.deliveryTags).get(this.deliveryTags.size() - 1); - this.channel.basicAck(deliveryTag, true); + try { + this.channel.basicAck(deliveryTag, true); + notifyMessageAckListener(messageAckListener, true, deliveryTag, null); + } + catch (Exception e) { + logger.error("Error acking.", e); + notifyMessageAckListener(messageAckListener, false, deliveryTag, e); + } } if (isLocallyTransacted) { @@ -874,6 +882,28 @@ public boolean commitIfNecessary(boolean localTx) throws IOException { } + /** + * Notify MessageAckListener set on the relevant message listener. + * @param messageAckListener MessageAckListener set on the message listener. + * @param success Whether ack succeeded. + * @param deliveryTag The deliveryTag of ack. + * @param cause If an exception occurs. + */ + private void notifyMessageAckListener(@Nullable MessageAckListener messageAckListener, + boolean success, + long deliveryTag, + @Nullable Throwable cause) { + if (messageAckListener == null) { + return; + } + try { + messageAckListener.onComplete(success, deliveryTag, cause); + } + catch (Exception e) { + logger.error("An exception occured on MessageAckListener.", e); + } + } + @Override public String toString() { return "Consumer@" + ObjectUtils.getIdentityHexString(this) + ": " diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java index 1823d17ddc..d6d1a23b14 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java @@ -92,6 +92,7 @@ * @author Gary Russell * @author Artem Bilan * @author Nicolas Ristock + * @author Cao Weibo * * @since 2.0 * @@ -531,7 +532,7 @@ private void checkConsumers(long now) { try { consumer.ackIfNecessary(now); } - catch (IOException e) { + catch (Exception e) { this.logger.error("Exception while sending delayed ack", e); } } @@ -882,7 +883,7 @@ private void cancelConsumer(SimpleConsumer consumer) { try { consumer.ackIfNecessary(0L); } - catch (IOException e) { + catch (Exception e) { this.logger.error("Exception while sending delayed ack", e); } } @@ -1175,7 +1176,7 @@ private void handleAck(long deliveryTag, boolean channelLocallyTransacted) { } } else if (!isChannelTransacted() || isLocallyTransacted) { - getChannel().basicAck(deliveryTag, false); + sendAckWithNotify(deliveryTag, false); } } if (isLocallyTransacted) { @@ -1194,7 +1195,7 @@ else if (!isChannelTransacted() || isLocallyTransacted) { * @param now the current time. * @throws IOException if one occurs. */ - synchronized void ackIfNecessary(long now) throws IOException { + synchronized void ackIfNecessary(long now) throws Exception { if (this.pendingAcks >= this.messagesPerAck || ( this.pendingAcks > 0 && (now - this.lastAck > this.ackTimeout || this.canceled))) { sendAck(now); @@ -1217,7 +1218,7 @@ private void rollback(long deliveryTag, Exception e) { getChannel().basicNack(deliveryTag, !isAsyncReplies(), ContainerUtils.shouldRequeue(isDefaultRequeueRejected(), e, this.logger)); } - catch (IOException e1) { + catch (Exception e1) { this.logger.error("Failed to nack message", e1); } } @@ -1226,12 +1227,51 @@ private void rollback(long deliveryTag, Exception e) { } } - protected synchronized void sendAck(long now) throws IOException { - getChannel().basicAck(this.latestDeferredDeliveryTag, true); + protected synchronized void sendAck(long now) throws Exception { + sendAckWithNotify(this.latestDeferredDeliveryTag, true); this.lastAck = now; this.pendingAcks = 0; } + /** + * Send ack and notify MessageAckListener(if set). + * @param deliveryTag DeliveryTag of this ack. + * @param multiple Whether multiple ack. + * @throws Exception Occured when ack. + */ + private void sendAckWithNotify(long deliveryTag, boolean multiple) throws Exception { + try { + getChannel().basicAck(deliveryTag, multiple); + notifyMessageAckListener(getMessageAckListener(), true, deliveryTag, null); + } + catch (Exception e) { + notifyMessageAckListener(getMessageAckListener(), false, deliveryTag, e); + throw e; + } + } + + /** + * Notify MessageAckListener set on message listener. + * @param messageAckListener MessageAckListener set on the message listener. + * @param success Whether ack succeeded. + * @param deliveryTag The deliveryTag of ack. + * @param cause If an exception occurs. + */ + private void notifyMessageAckListener(@Nullable MessageAckListener messageAckListener, + boolean success, + long deliveryTag, + @Nullable Throwable cause) { + if (messageAckListener == null) { + return; + } + try { + messageAckListener.onComplete(success, deliveryTag, cause); + } + catch (Exception e) { + this.logger.error("An exception occured on MessageAckListener.", e); + } + } + @Override public void handleConsumeOk(String consumerTag) { super.handleConsumeOk(consumerTag); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MessageAckListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MessageAckListener.java new file mode 100644 index 0000000000..428cf769e8 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MessageAckListener.java @@ -0,0 +1,41 @@ +/* + * Copyright 2021 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; + +import org.springframework.amqp.core.AcknowledgeMode; +import org.springframework.lang.Nullable; + +/** + * A listener for message ack when using {@link AcknowledgeMode#AUTO}. + * + * @author Cao Weibo + * @see AbstractMessageListenerContainer#setMessageAckListener(MessageAckListener) + */ +@FunctionalInterface +public interface MessageAckListener { + + /** + * Listener callback. + * @param success Whether ack succeed. + * @param deliveryTag The deliveryTag of ack. + * @param cause The cause of failed ack. + * + * @throws Exception the exception during callback. + */ + void onComplete(boolean success, long deliveryTag, @Nullable Throwable cause) throws Exception; + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java index ed9cfc518e..c0d6a976db 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java @@ -1044,7 +1044,7 @@ private boolean doReceiveAndExecute(BlockingQueueConsumer consumer) throws Excep executeWithList(channel, messages, deliveryTag, consumer); } - return consumer.commitIfNecessary(isChannelLocallyTransacted()); + return consumer.commitIfNecessary(isChannelLocallyTransacted(), getMessageAckListener()); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java index 7c92cf4f97..9116fe1130 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java @@ -36,6 +36,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import org.aopalliance.intercept.MethodInterceptor; @@ -47,6 +48,8 @@ import org.mockito.ArgumentCaptor; import org.springframework.amqp.AmqpAuthenticationException; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.core.Queue; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.Connection; @@ -80,6 +83,7 @@ * @author Gary Russell * @author Artem Bilan * @author Alex Panchenko + * @author Cao Weibo * * @since 2.0 * @@ -720,6 +724,118 @@ else if (event instanceof AsyncConsumerStartedEvent) { cf.destroy(); } + @Test + public void testMessageAckListenerWithSuccessfulAck() throws Exception { + CachingConnectionFactory cf = new CachingConnectionFactory("localhost"); + DirectMessageListenerContainer container = new DirectMessageListenerContainer(cf); + final AtomicInteger calledTimes = new AtomicInteger(); + final AtomicReference ackDeliveryTag = new AtomicReference<>(); + final AtomicReference ackSuccess = new AtomicReference<>(); + final AtomicReference ackCause = new AtomicReference<>(); + final CountDownLatch latch = new CountDownLatch(1); + container.setQueueNames(Q1); + container.setMessageListener(new MessageListener() { + @Override + public void onMessage(Message message) { + } + }); + container.setMessageAckListener(new MessageAckListener() { + @Override + public void onComplete(boolean success, long deliveryTag, Throwable cause) throws Exception { + calledTimes.incrementAndGet(); + ackDeliveryTag.set(deliveryTag); + ackSuccess.set(success); + ackCause.set(cause); + latch.countDown(); + } + }); + container.start(); + RabbitTemplate rabbitTemplate = new RabbitTemplate(cf); + final int messageCount = 5; + for (int i = 0; i < messageCount; i++) { + rabbitTemplate.convertAndSend(Q1, "foo"); + } + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + container.stop(); + assertThat(calledTimes.get()).isEqualTo(messageCount); + assertThat(ackSuccess.get()).isTrue(); + assertThat(ackCause.get()).isNull(); + assertThat(ackDeliveryTag.get()).isEqualTo(messageCount); + } + + @Test + public void testMessageAckListenerWithFailedAck() throws Exception { + CachingConnectionFactory cf = new CachingConnectionFactory("localhost"); + DirectMessageListenerContainer container = new DirectMessageListenerContainer(cf); + final AtomicReference called = new AtomicReference<>(); + final AtomicReference ackDeliveryTag = new AtomicReference<>(); + final AtomicReference ackSuccess = new AtomicReference<>(); + final AtomicReference ackCause = new AtomicReference<>(); + final CountDownLatch latch = new CountDownLatch(1); + container.setQueueNames(Q1); + container.setMessageListener(new MessageListener() { + @Override + public void onMessage(Message message) { + cf.resetConnection(); + } + }); + container.setMessageAckListener(new MessageAckListener() { + @Override + public void onComplete(boolean success, long deliveryTag, Throwable cause) throws Exception { + called.set(true); + ackDeliveryTag.set(deliveryTag); + ackSuccess.set(success); + ackCause.set(cause); + latch.countDown(); + } + }); + container.start(); + new RabbitTemplate(cf).convertAndSend(Q1, "foo"); + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + container.stop(); + assertThat(called.get()).isTrue(); + assertThat(ackSuccess.get()).isFalse(); + assertThat(ackCause.get().getMessage()).isEqualTo("Channel closed; cannot ack/nack"); + assertThat(ackDeliveryTag.get()).isEqualTo(1); + } + + @Test + public void testMessageAckListenerWithBatchAck() throws Exception { + final AtomicInteger calledTimes = new AtomicInteger(); + final AtomicReference ackDeliveryTag = new AtomicReference<>(); + final AtomicReference ackSuccess = new AtomicReference<>(); + final AtomicReference ackCause = new AtomicReference<>(); + CachingConnectionFactory cf = new CachingConnectionFactory("localhost"); + DirectMessageListenerContainer container = new DirectMessageListenerContainer(cf); + final int messageCount = 5; + final CountDownLatch latch = new CountDownLatch(1); + container.setQueueNames(Q1); + container.setMessagesPerAck(messageCount); + container.setMessageListener(message -> { + }); + container.setMessageAckListener(new MessageAckListener() { + @Override + public void onComplete(boolean success, long deliveryTag, Throwable cause) throws Exception { + calledTimes.incrementAndGet(); + ackDeliveryTag.set(deliveryTag); + ackSuccess.set(success); + ackCause.set(cause); + latch.countDown(); + } + }); + container.start(); + RabbitTemplate rabbitTemplate = new RabbitTemplate(cf); + for (int i = 0; i < messageCount; i++) { + rabbitTemplate.convertAndSend(Q1, "foo"); + } + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + container.stop(); + assertThat(calledTimes.get()).isEqualTo(1); + assertThat(ackSuccess.get()).isTrue(); + assertThat(ackCause.get()).isNull(); + assertThat(ackDeliveryTag.get()).isEqualTo(messageCount); + } + private boolean consumersOnQueue(String queue, int expected) throws Exception { await().with().pollDelay(Duration.ZERO).atMost(Duration.ofSeconds(60)) .until(() -> admin.getQueueProperties(queue), diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java index 42ccdc011c..3c92ea9977 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java @@ -53,6 +53,7 @@ import org.springframework.amqp.AmqpIOException; import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.AnonymousQueue; +import org.springframework.amqp.core.BatchMessageListener; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.core.Queue; @@ -87,6 +88,7 @@ * @author Gunnar Hillert * @author Gary Russell * @author Artem Bilan + * @author Cao Weibo * * @since 1.3 * @@ -681,6 +683,76 @@ public void testManualAckWithClosedChannel() throws Exception { assertThat(exc.get().getMessage()).isEqualTo("Channel closed; cannot ack/nack"); } + @Test + public void testMessageAckListenerWithSuccessfulAck() throws Exception { + final AtomicInteger calledTimes = new AtomicInteger(); + final AtomicReference ackDeliveryTag = new AtomicReference<>(); + final AtomicReference ackSuccess = new AtomicReference<>(); + final AtomicReference ackCause = new AtomicReference<>(); + final CountDownLatch latch = new CountDownLatch(1); + this.container = createContainer((ChannelAwareMessageListener) (m, c) -> { + }, false, this.queue.getName()); + this.container.setAcknowledgeMode(AcknowledgeMode.AUTO); + this.container.afterPropertiesSet(); + this.container.start(); + this.container.setMessageAckListener(new MessageAckListener() { + @Override + public void onComplete(boolean success, long deliveryTag, Throwable cause) throws Exception { + calledTimes.incrementAndGet(); + ackDeliveryTag.set(deliveryTag); + ackSuccess.set(success); + ackCause.set(cause); + latch.countDown(); + } + }); + int messageCount = 5; + for (int i = 0; i < messageCount; i++) { + this.template.convertAndSend(this.queue.getName(), "foo"); + } + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + this.container.stop(); + assertThat(calledTimes.get()).isEqualTo(messageCount); + assertThat(ackSuccess.get()).isTrue(); + assertThat(ackCause.get()).isNull(); + assertThat(ackDeliveryTag.get()).isEqualTo(messageCount); + } + + @Test + public void testMessageAckListenerWithBatchAck() throws Exception { + final AtomicInteger calledTimes = new AtomicInteger(); + final AtomicReference ackDeliveryTag = new AtomicReference<>(); + final AtomicReference ackSuccess = new AtomicReference<>(); + final AtomicReference ackCause = new AtomicReference<>(); + final CountDownLatch latch = new CountDownLatch(1); + this.container = createContainer((BatchMessageListener) messages -> { + }, false, this.queue.getName()); + this.container.setBatchSize(5); + this.container.setConsumerBatchEnabled(true); + this.container.setAcknowledgeMode(AcknowledgeMode.AUTO); + this.container.afterPropertiesSet(); + this.container.start(); + this.container.setMessageAckListener(new MessageAckListener() { + @Override + public void onComplete(boolean success, long deliveryTag, Throwable cause) throws Exception { + calledTimes.incrementAndGet(); + ackDeliveryTag.set(deliveryTag); + ackSuccess.set(success); + ackCause.set(cause); + latch.countDown(); + } + }); + int messageCount = 5; + for (int i = 0; i < messageCount; i++) { + this.template.convertAndSend(this.queue.getName(), "foo"); + } + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + this.container.stop(); + assertThat(calledTimes.get()).isEqualTo(1); + assertThat(ackSuccess.get()).isTrue(); + assertThat(ackCause.get()).isNull(); + assertThat(ackDeliveryTag.get()).isEqualTo(messageCount); + } + private boolean containerStoppedForAbortWithBadListener() throws InterruptedException { Log logger = spy(TestUtils.getPropertyValue(container, "logger", Log.class)); new DirectFieldAccessor(container).setPropertyValue("logger", logger); From 247c45ee7e47009a474819407c643f617c1fae7f Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 15 Jun 2022 14:49:08 -0400 Subject: [PATCH 110/737] GH-1338: Polishing - Add a default ack listener - Use lambdas in tests - NOSONAR tags --- ...bstractRabbitListenerContainerFactory.java | 5 +- .../AbstractMessageListenerContainer.java | 6 ++- .../listener/BlockingQueueConsumer.java | 37 +++++++++------ .../DirectMessageListenerContainer.java | 22 ++++----- .../rabbit/listener/MessageAckListener.java | 2 +- .../SimpleMessageListenerContainer.java | 3 +- ...sageListenerContainerIntegrationTests.java | 47 ++++++++----------- ...ageListenerContainerIntegration2Tests.java | 36 ++++++-------- 8 files changed, 76 insertions(+), 82 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java index 520a3752af..69d6874173 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java @@ -327,9 +327,10 @@ public void setGlobalQos(boolean globalQos) { } /** - * Set a {@link MessageAckListener} to use when ack a message(messages) in {@link AcknowledgeMode#AUTO} mode. + * Set a {@link MessageAckListener} to use when ack a message(messages) in + * {@link AcknowledgeMode#AUTO} mode. * @param messageAckListener the messageAckListener. - * @see AbstractMessageListenerContainer#setMessageAckListener(MessageAckListener) + * @since 2.4.6 */ public void setMessageAckListener(MessageAckListener messageAckListener) { this.messageAckListener = messageAckListener; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index ee75ca57c2..b4c4d8688d 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -252,7 +252,7 @@ public abstract class AbstractMessageListenerContainer extends RabbitAccessor private boolean asyncReplies; - private MessageAckListener messageAckListener; + private MessageAckListener messageAckListener = (success, deliveryTag, cause) -> { }; @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { @@ -1191,8 +1191,10 @@ public void setjavaLangErrorHandler(JavaLangErrorHandler javaLangErrorHandler) { } /** - * Set a {@link MessageAckListener} to use when ack a message(messages) in {@link AcknowledgeMode#AUTO} mode. + * Set a {@link MessageAckListener} to use when ack a message(messages) in + * {@link AcknowledgeMode#AUTO} mode. * @param messageAckListener the messageAckListener. + * @since 2.4.6 * @see MessageAckListener * @see AcknowledgeMode */ diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java index 47d502b5bf..0e3920169d 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java @@ -65,6 +65,7 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.lang.Nullable; import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.backoff.BackOffExecution; @@ -173,6 +174,8 @@ public class BlockingQueueConsumer { volatile boolean declaring; // NOSONAR package protected + private MessageAckListener messageAckListener; + /** * Create a consumer. The consumer must not attempt to use * the connection factory or communicate with the broker @@ -189,6 +192,7 @@ public BlockingQueueConsumer(ConnectionFactory connectionFactory, MessagePropertiesConverter messagePropertiesConverter, ActiveObjectCounter activeObjectCounter, AcknowledgeMode acknowledgeMode, boolean transactional, int prefetchCount, String... queues) { + this(connectionFactory, messagePropertiesConverter, activeObjectCounter, acknowledgeMode, transactional, prefetchCount, true, queues); } @@ -399,6 +403,17 @@ public void setConsumeDelay(long consumeDelay) { this.consumeDelay = consumeDelay; } + /** + * Set a {@link MessageAckListener} to use when ack a message(messages) in + * {@link AcknowledgeMode#AUTO} mode. + * @param messageAckListener the messageAckListener. + * @since 2.4.6 + */ + public void setMessageAckListener(MessageAckListener messageAckListener) { + Assert.notNull(messageAckListener, "'messageAckListener' cannot be null"); + this.messageAckListener = messageAckListener; + } + /** * Clear the delivery tags when rolling back with an external transaction * manager. @@ -841,7 +856,7 @@ public void rollbackOnExceptionIfNecessary(Throwable ex, long tag) { * @return true if at least one delivery tag exists. * @throws IOException Any IOException. */ - public boolean commitIfNecessary(boolean localTx, MessageAckListener messageAckListener) throws IOException { + public boolean commitIfNecessary(boolean localTx) throws IOException { if (this.deliveryTags.isEmpty()) { return false; } @@ -860,11 +875,11 @@ public boolean commitIfNecessary(boolean localTx, MessageAckListener messageAckL long deliveryTag = new ArrayList(this.deliveryTags).get(this.deliveryTags.size() - 1); try { this.channel.basicAck(deliveryTag, true); - notifyMessageAckListener(messageAckListener, true, deliveryTag, null); + notifyMessageAckListener(true, deliveryTag, null); } catch (Exception e) { logger.error("Error acking.", e); - notifyMessageAckListener(messageAckListener, false, deliveryTag, e); + notifyMessageAckListener(false, deliveryTag, e); } } @@ -883,24 +898,18 @@ public boolean commitIfNecessary(boolean localTx, MessageAckListener messageAckL } /** - * Notify MessageAckListener set on the relevant message listener. - * @param messageAckListener MessageAckListener set on the message listener. + * Notify MessageAckListener set on message listener. * @param success Whether ack succeeded. * @param deliveryTag The deliveryTag of ack. * @param cause If an exception occurs. + * @since 2.4.6 */ - private void notifyMessageAckListener(@Nullable MessageAckListener messageAckListener, - boolean success, - long deliveryTag, - @Nullable Throwable cause) { - if (messageAckListener == null) { - return; - } + private void notifyMessageAckListener(boolean success, long deliveryTag, @Nullable Throwable cause) { try { - messageAckListener.onComplete(success, deliveryTag, cause); + this.messageAckListener.onComplete(success, deliveryTag, cause); } catch (Exception e) { - logger.error("An exception occured on MessageAckListener.", e); + logger.error("An exception occured in MessageAckListener.", e); } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java index d6d1a23b14..a41fec74aa 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java @@ -1195,7 +1195,7 @@ else if (!isChannelTransacted() || isLocallyTransacted) { * @param now the current time. * @throws IOException if one occurs. */ - synchronized void ackIfNecessary(long now) throws Exception { + synchronized void ackIfNecessary(long now) throws Exception { // NOSONAR if (this.pendingAcks >= this.messagesPerAck || ( this.pendingAcks > 0 && (now - this.lastAck > this.ackTimeout || this.canceled))) { sendAck(now); @@ -1227,7 +1227,7 @@ private void rollback(long deliveryTag, Exception e) { } } - protected synchronized void sendAck(long now) throws Exception { + protected synchronized void sendAck(long now) throws Exception { // NOSONAR sendAckWithNotify(this.latestDeferredDeliveryTag, true); this.lastAck = now; this.pendingAcks = 0; @@ -1238,14 +1238,15 @@ protected synchronized void sendAck(long now) throws Exception { * @param deliveryTag DeliveryTag of this ack. * @param multiple Whether multiple ack. * @throws Exception Occured when ack. + * @Since 2.4.6 */ - private void sendAckWithNotify(long deliveryTag, boolean multiple) throws Exception { + private void sendAckWithNotify(long deliveryTag, boolean multiple) throws Exception { // NOSONAR try { getChannel().basicAck(deliveryTag, multiple); - notifyMessageAckListener(getMessageAckListener(), true, deliveryTag, null); + notifyMessageAckListener(true, deliveryTag, null); } catch (Exception e) { - notifyMessageAckListener(getMessageAckListener(), false, deliveryTag, e); + notifyMessageAckListener(false, deliveryTag, e); throw e; } } @@ -1256,16 +1257,11 @@ private void sendAckWithNotify(long deliveryTag, boolean multiple) throws Except * @param success Whether ack succeeded. * @param deliveryTag The deliveryTag of ack. * @param cause If an exception occurs. + * @since 2.4.6 */ - private void notifyMessageAckListener(@Nullable MessageAckListener messageAckListener, - boolean success, - long deliveryTag, - @Nullable Throwable cause) { - if (messageAckListener == null) { - return; - } + private void notifyMessageAckListener(boolean success, long deliveryTag, @Nullable Throwable cause) { try { - messageAckListener.onComplete(success, deliveryTag, cause); + getMessageAckListener().onComplete(success, deliveryTag, cause); } catch (Exception e) { this.logger.error("An exception occured on MessageAckListener.", e); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MessageAckListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MessageAckListener.java index 428cf769e8..5031a80ab3 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MessageAckListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MessageAckListener.java @@ -23,7 +23,7 @@ * A listener for message ack when using {@link AcknowledgeMode#AUTO}. * * @author Cao Weibo - * @see AbstractMessageListenerContainer#setMessageAckListener(MessageAckListener) + * @since 2.4.6 */ @FunctionalInterface public interface MessageAckListener { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java index c0d6a976db..201a371dba 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java @@ -865,6 +865,7 @@ this.cancellationLock, getAcknowledgeMode(), isChannelTransacted(), actualPrefet consumer.setBackOffExecution(getRecoveryBackOff().start()); consumer.setShutdownTimeout(getShutdownTimeout()); consumer.setApplicationEventPublisher(getApplicationEventPublisher()); + consumer.setMessageAckListener(getMessageAckListener()); return consumer; } @@ -1044,7 +1045,7 @@ private boolean doReceiveAndExecute(BlockingQueueConsumer consumer) throws Excep executeWithList(channel, messages, deliveryTag, consumer); } - return consumer.commitIfNecessary(isChannelLocallyTransacted(), getMessageAckListener()); + return consumer.commitIfNecessary(isChannelLocallyTransacted()); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java index 9116fe1130..27b1824b94 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 the original author or authors. + * Copyright 2016-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. @@ -739,15 +739,12 @@ public void testMessageAckListenerWithSuccessfulAck() throws Exception { public void onMessage(Message message) { } }); - container.setMessageAckListener(new MessageAckListener() { - @Override - public void onComplete(boolean success, long deliveryTag, Throwable cause) throws Exception { - calledTimes.incrementAndGet(); - ackDeliveryTag.set(deliveryTag); - ackSuccess.set(success); - ackCause.set(cause); - latch.countDown(); - } + container.setMessageAckListener((success, deliveryTag, cause) -> { + calledTimes.incrementAndGet(); + ackDeliveryTag.set(deliveryTag); + ackSuccess.set(success); + ackCause.set(cause); + latch.countDown(); }); container.start(); RabbitTemplate rabbitTemplate = new RabbitTemplate(cf); @@ -779,15 +776,12 @@ public void onMessage(Message message) { cf.resetConnection(); } }); - container.setMessageAckListener(new MessageAckListener() { - @Override - public void onComplete(boolean success, long deliveryTag, Throwable cause) throws Exception { - called.set(true); - ackDeliveryTag.set(deliveryTag); - ackSuccess.set(success); - ackCause.set(cause); - latch.countDown(); - } + container.setMessageAckListener((success, deliveryTag, cause) -> { + called.set(true); + ackDeliveryTag.set(deliveryTag); + ackSuccess.set(success); + ackCause.set(cause); + latch.countDown(); }); container.start(); new RabbitTemplate(cf).convertAndSend(Q1, "foo"); @@ -813,15 +807,12 @@ public void testMessageAckListenerWithBatchAck() throws Exception { container.setMessagesPerAck(messageCount); container.setMessageListener(message -> { }); - container.setMessageAckListener(new MessageAckListener() { - @Override - public void onComplete(boolean success, long deliveryTag, Throwable cause) throws Exception { - calledTimes.incrementAndGet(); - ackDeliveryTag.set(deliveryTag); - ackSuccess.set(success); - ackCause.set(cause); - latch.countDown(); - } + container.setMessageAckListener((success, deliveryTag, cause) -> { + calledTimes.incrementAndGet(); + ackDeliveryTag.set(deliveryTag); + ackSuccess.set(success); + ackCause.set(cause); + latch.countDown(); }); container.start(); RabbitTemplate rabbitTemplate = new RabbitTemplate(cf); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java index 3c92ea9977..d80eb17b8e 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -693,18 +693,15 @@ public void testMessageAckListenerWithSuccessfulAck() throws Exception { this.container = createContainer((ChannelAwareMessageListener) (m, c) -> { }, false, this.queue.getName()); this.container.setAcknowledgeMode(AcknowledgeMode.AUTO); + this.container.setMessageAckListener((success, deliveryTag, cause) -> { + calledTimes.incrementAndGet(); + ackDeliveryTag.set(deliveryTag); + ackSuccess.set(success); + ackCause.set(cause); + latch.countDown(); + }); this.container.afterPropertiesSet(); this.container.start(); - this.container.setMessageAckListener(new MessageAckListener() { - @Override - public void onComplete(boolean success, long deliveryTag, Throwable cause) throws Exception { - calledTimes.incrementAndGet(); - ackDeliveryTag.set(deliveryTag); - ackSuccess.set(success); - ackCause.set(cause); - latch.countDown(); - } - }); int messageCount = 5; for (int i = 0; i < messageCount; i++) { this.template.convertAndSend(this.queue.getName(), "foo"); @@ -729,18 +726,15 @@ public void testMessageAckListenerWithBatchAck() throws Exception { this.container.setBatchSize(5); this.container.setConsumerBatchEnabled(true); this.container.setAcknowledgeMode(AcknowledgeMode.AUTO); + this.container.setMessageAckListener((success, deliveryTag, cause) -> { + calledTimes.incrementAndGet(); + ackDeliveryTag.set(deliveryTag); + ackSuccess.set(success); + ackCause.set(cause); + latch.countDown(); + }); this.container.afterPropertiesSet(); this.container.start(); - this.container.setMessageAckListener(new MessageAckListener() { - @Override - public void onComplete(boolean success, long deliveryTag, Throwable cause) throws Exception { - calledTimes.incrementAndGet(); - ackDeliveryTag.set(deliveryTag); - ackSuccess.set(success); - ackCause.set(cause); - latch.countDown(); - } - }); int messageCount = 5; for (int i = 0; i < messageCount; i++) { this.template.convertAndSend(this.queue.getName(), "foo"); From ac6242921282bb87768d7b767b72bb5a0ebbf806 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 15 Jun 2022 15:03:33 -0400 Subject: [PATCH 111/737] GH-1338: Fix Javadoc --- .../amqp/rabbit/listener/BlockingQueueConsumer.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java index 0e3920169d..e1359b3b39 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -852,7 +852,6 @@ public void rollbackOnExceptionIfNecessary(Throwable ex, long tag) { /** * Perform a commit or message acknowledgement, as appropriate. * @param localTx Whether the channel is locally transacted. - * @param messageAckListener MessageAckListener set on the message listener. * @return true if at least one delivery tag exists. * @throws IOException Any IOException. */ From e25600b8969fff69737acf222c0f8c3dc39ea04f Mon Sep 17 00:00:00 2001 From: Ankush Gatfane Date: Wed, 4 Nov 2020 19:03:55 +0530 Subject: [PATCH 112/737] Fix AbstractMLC.getMessageListener Return Type Getter For MessageListener Tuned With Its Setter Now that we have removed deprecated setMessageListener, which used to accept an argument of type 'Object', and now that we have only one setter which accepts an argument of type `MessageListener`, it now makes sense to return `MessageListener` from getter method. This not only makes getter and setter tuned but would fix the problem wherein the getter and setter required to be working on the same parameter type. e.g. in jxm library. --- .../rabbit/listener/AbstractMessageListenerContainer.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index b4c4d8688d..4e276dd05c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -458,7 +458,11 @@ protected void checkMessageListener(Object listener) { @Override @Nullable - public Object getMessageListener() { + /** + * Get a reference to the message listener. + * @return the message listener. + */ + public MessageListener getMessageListener() { return this.messageListener; } From 50624103fa88ad304fdd08f72923507d6c953487 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 16 Jun 2022 12:10:54 -0400 Subject: [PATCH 113/737] Fix Sonar Issues --- .../rabbit/stream/listener/StreamListenerContainer.java | 4 +++- .../amqp/rabbit/listener/MessageAckListener.java | 5 ++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java index 67d51196a9..21401afbec 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java @@ -16,6 +16,8 @@ package org.springframework.rabbit.stream.listener; +import java.util.Arrays; + import org.aopalliance.aop.Advice; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -170,7 +172,7 @@ public boolean isAutoStartup() { public void setAdviceChain(Advice... advices) { Assert.notNull(advices, "'advices' cannot be null"); Assert.noNullElements(advices, "'advices' cannot have null elements"); - this.adviceChain = advices; + this.adviceChain = Arrays.copyOf(adviceChain, adviceChain.length); } @Override diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MessageAckListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MessageAckListener.java index 5031a80ab3..3c63904e19 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MessageAckListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MessageAckListener.java @@ -23,6 +23,7 @@ * A listener for message ack when using {@link AcknowledgeMode#AUTO}. * * @author Cao Weibo + * @author Gary Russell * @since 2.4.6 */ @FunctionalInterface @@ -33,9 +34,7 @@ public interface MessageAckListener { * @param success Whether ack succeed. * @param deliveryTag The deliveryTag of ack. * @param cause The cause of failed ack. - * - * @throws Exception the exception during callback. */ - void onComplete(boolean success, long deliveryTag, @Nullable Throwable cause) throws Exception; + void onComplete(boolean success, long deliveryTag, @Nullable Throwable cause); } From 6dae0fc094f6135dde1ddedd4dada65e172f6de7 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 16 Jun 2022 12:13:32 -0400 Subject: [PATCH 114/737] Fix Previous Commit --- .../rabbit/stream/listener/StreamListenerContainer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java index 21401afbec..2a9684ccde 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java @@ -172,7 +172,7 @@ public boolean isAutoStartup() { public void setAdviceChain(Advice... advices) { Assert.notNull(advices, "'advices' cannot be null"); Assert.noNullElements(advices, "'advices' cannot have null elements"); - this.adviceChain = Arrays.copyOf(adviceChain, adviceChain.length); + this.adviceChain = Arrays.copyOf(advices, advices.length); } @Override From d143d4b7ae2495a19ce1fb72edeb4e6503da9afd Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 16 Jun 2022 13:18:33 -0400 Subject: [PATCH 115/737] GH-1465: Part 1: Provision Super Streams over AMQP See https://github.com/spring-projects/spring-amqp/issues/1465 Inspired by https://github.com/rabbitmq/rabbitmq-stream-java-client/blob/29b1a3eca72c8e3f719db4fd9fafdf503f37ea20/src/test/java/com/rabbitmq/stream/impl/TestUtils.java#L244-L276 * Docs and Polishing. - address PR review - move abstract test class to `support` --- .../rabbit/stream/config/SuperStream.java | 64 +++++++++++++++ .../config/SuperStreamProvisioningTests.java | 77 +++++++++++++++++++ .../stream/listener/RabbitListenerTests.java | 1 + .../AbstractIntegrationTests.java | 8 +- src/reference/asciidoc/stream.adoc | 24 ++++++ 5 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStream.java create mode 100644 spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamProvisioningTests.java rename spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/{listener => support}/AbstractIntegrationTests.java (91%) diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStream.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStream.java new file mode 100644 index 0000000000..73fe76a9ff --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStream.java @@ -0,0 +1,64 @@ +/* + * 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.rabbit.stream.config; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.IntStream; + +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.Binding.DestinationType; +import org.springframework.amqp.core.Declarable; +import org.springframework.amqp.core.Declarables; +import org.springframework.amqp.core.DirectExchange; +import org.springframework.amqp.core.Queue; + +/** + * Create Super Stream Topology {@link Declarable}s. + * + * @author Gary Russell + * @since 3.0 + * + */ +public class SuperStream extends Declarables { + + /** + * Create a Super Stream with the provided parameters. + * @param name the stream name. + * @param partitions the number of partitions. + */ + public SuperStream(String name, int partitions) { + super(declarables(name, partitions)); + } + + private static Collection declarables(String name, int partitions) { + List declarables = new ArrayList<>(); + String[] rks = IntStream.range(0, partitions).mapToObj(String::valueOf).toArray(String[]::new); + declarables.add(new DirectExchange(name, true, false, Map.of("x-super-stream", true))); + for (int i = 0; i < partitions; i++) { + String rk = rks[i]; + Queue q = new Queue(name + "-" + rk, true, false, false, Map.of("x-queue-type", "stream")); + declarables.add(q); + declarables.add(new Binding(q.getName(), DestinationType.QUEUE, name, rk, + Map.of("x-stream-partition-order", i))); + } + return declarables; + } + +} diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamProvisioningTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamProvisioningTests.java new file mode 100644 index 0000000000..b96eb88125 --- /dev/null +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamProvisioningTests.java @@ -0,0 +1,77 @@ +/* + * 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.rabbit.stream.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.core.Declarables; +import org.springframework.amqp.core.DirectExchange; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.rabbit.stream.support.AbstractIntegrationTests; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +/** + * @author Gary Russell + * @since 3.0 + * + */ +@SpringJUnitConfig +public class SuperStreamProvisioningTests extends AbstractIntegrationTests { + + @Test + void provision(@Autowired Declarables declarables, @Autowired CachingConnectionFactory cf, + @Autowired RabbitAdmin admin) { + + assertThat(declarables.getDeclarables()).hasSize(7); + cf.createConnection(); + List queues = declarables.getDeclarablesByType(Queue.class); + assertThat(queues).extracting(que -> que.getName()).contains("test-0", "test-1", "test-2"); + queues.forEach(que -> admin.deleteQueue(que.getName())); + declarables.getDeclarablesByType(DirectExchange.class).forEach(ex -> admin.deleteExchange(ex.getName())); + } + + @Configuration + public static class Config { + + @Bean + CachingConnectionFactory cf() { + return new CachingConnectionFactory("localhost", amqpPort()); + } + + @Bean + RabbitAdmin admin(ConnectionFactory cf) { + return new RabbitAdmin(cf); + } + + @Bean + SuperStream superStream() { + return new SuperStream("test", 3); + } + + } + +} diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java index b0f914d7ba..cd6d363bc8 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java @@ -44,6 +44,7 @@ import org.springframework.rabbit.stream.config.StreamRabbitListenerContainerFactory; import org.springframework.rabbit.stream.producer.RabbitStreamTemplate; import org.springframework.rabbit.stream.retry.StreamRetryOperationsInterceptorFactoryBean; +import org.springframework.rabbit.stream.support.AbstractIntegrationTests; import org.springframework.rabbit.stream.support.StreamMessageProperties; import org.springframework.retry.interceptor.RetryOperationsInterceptor; import org.springframework.test.annotation.DirtiesContext; diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/AbstractIntegrationTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/AbstractIntegrationTests.java similarity index 91% rename from spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/AbstractIntegrationTests.java rename to spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/AbstractIntegrationTests.java index 8739017fc2..e0c7f04dd7 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/AbstractIntegrationTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/AbstractIntegrationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.rabbit.stream.listener; +package org.springframework.rabbit.stream.support; import java.time.Duration; @@ -48,15 +48,15 @@ public abstract class AbstractIntegrationTests { } } - static int amqpPort() { + public static int amqpPort() { return RABBITMQ != null ? RABBITMQ.getMappedPort(5672) : 5672; } - static int managementPort() { + public static int managementPort() { return RABBITMQ != null ? RABBITMQ.getMappedPort(15672) : 15672; } - static int streamPort() { + public static int streamPort() { return RABBITMQ != null ? RABBITMQ.getMappedPort(5552) : 5552; } diff --git a/src/reference/asciidoc/stream.adoc b/src/reference/asciidoc/stream.adoc index 5e24395bee..a77374330b 100644 --- a/src/reference/asciidoc/stream.adoc +++ b/src/reference/asciidoc/stream.adoc @@ -157,3 +157,27 @@ public StreamRetryOperationsInterceptorFactoryBean sfb(RetryTemplate retryTempla ==== IMPORTANT: Stateful retry is not supported with this container. + +==== Super Streams + +A Super Stream is an abstract concept for a partitioned stream, implemented by binding a number of stream queues to an exchange having an argument `x-super-stream: true`. + +===== Provisioning + +For convenience, a super stream can be provisioned by defining a single bean of type `SuperStream`. + +==== +[source, java] +---- +@Bean +SuperStream superStream() { + return new SuperStream("my.super.stream", 3); +} +---- +==== + +The `RabbitAdmin` detects this bean and will declare the exchange (`my.super.stream`) and 3 queues (partitions) - `my.super-stream-n` where `n` is `0`, `1`, `2`, bound with routing keys equal to `n`. + +===== Consuming Super Streams with Single Active Consumers + +TBD. From 6afdfac4d5fc382580a4d934ee565de4654f4f70 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 19 Jul 2022 11:54:30 -0400 Subject: [PATCH 116/737] Upgrade Kotlin to 1.7 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 2ce43489c6..59c82f5c50 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlinVersion = '1.5.31' + ext.kotlinVersion = '1.7.0' repositories { mavenCentral() gradlePluginPortal() @@ -415,7 +415,7 @@ project('spring-rabbit') { compileTestKotlin { kotlinOptions { - jvmTarget = '16' + jvmTarget = '17' } } From 38103d6950a5a62c5ccb32c72b3cb8531b3fc137 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 19 Jul 2022 12:43:39 -0400 Subject: [PATCH 117/737] Fix Test for Latest Spring Framework 6.0 Nested runtime exception messages no longer include causes. --- build.gradle | 2 +- .../amqp/rabbit/core/RabbitMessagingTemplateTests.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 59c82f5c50..caeb838ee5 100644 --- a/build.gradle +++ b/build.gradle @@ -64,7 +64,7 @@ ext { reactorVersion = '2020.0.18' snappyVersion = '1.1.8.4' springDataVersion = '2022.0.0-M3' - springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.0-M4' + springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.0-SNAPSHOT' springRetryVersion = '1.3.3' zstdJniVersion = '1.5.0-2' } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplateTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplateTests.java index d42b134472..4544864383 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplateTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplateTests.java @@ -211,7 +211,7 @@ protected org.springframework.amqp.core.Message createMessage(Object object, assertThatThrownBy(() -> messagingTemplate.convertAndSend("myQueue", "msg to convert")) .isExactlyInstanceOf(org.springframework.messaging.converter.MessageConversionException.class) - .hasMessageContaining("Test exception"); + .hasStackTraceContaining("Test exception"); } @Test From d538c3902afc283aaba837b26bf7193e68e3945e Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 19 Jul 2022 13:03:15 -0400 Subject: [PATCH 118/737] Fix Another Test with SF 6 --- .../amqp/rabbit/listener/ContainerInitializationTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerInitializationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerInitializationTests.java index 95dd3e2e3a..e5b0bfa4d8 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerInitializationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerInitializationTests.java @@ -63,7 +63,7 @@ public void testNoAdmin() { } catch (ApplicationContextException e) { assertThat(e.getCause().getCause()).isInstanceOf(IllegalStateException.class); - assertThat(e.getMessage()).contains("When 'mismatchedQueuesFatal' is 'true', there must be " + assertThat(e.getCause().getMessage()).contains("When 'mismatchedQueuesFatal' is 'true', there must be " + "exactly one AmqpAdmin in the context or you must inject one into this container; found: 0"); } } From 32c1c996c018622ddc4c6191c25922ece40fb1b5 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 19 Jul 2022 13:32:14 -0400 Subject: [PATCH 119/737] Resolve Deprecations in Spring Framework 6.0 - TaskScheduler --- .../amqp/rabbit/AsyncRabbitTemplate.java | 6 +++--- .../amqp/rabbit/core/BatchingRabbitTemplate.java | 13 +++++++------ .../listener/DirectMessageListenerContainer.java | 11 ++++++----- .../listener/ContainerInitializationTests.java | 2 +- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java index 21b08d00ec..3d31b4cd65 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 the original author or authors. + * Copyright 2016-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. @@ -16,7 +16,7 @@ package org.springframework.amqp.rabbit; -import java.util.Date; +import java.time.Instant; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -741,7 +741,7 @@ void startTimer() { throw new IllegalStateException("'AsyncRabbitTemplate' must be started."); } this.timeoutTask = AsyncRabbitTemplate.this.taskScheduler.schedule(new TimeoutTask(), - new Date(System.currentTimeMillis() + AsyncRabbitTemplate.this.receiveTimeout)); + Instant.now().plusMillis(AsyncRabbitTemplate.this.receiveTimeout)); } } else { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplate.java index 0b19f09519..8316b3e375 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 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. @@ -25,6 +25,7 @@ import org.springframework.amqp.rabbit.batch.MessageBatch; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.connection.CorrelationData; +import org.springframework.lang.Nullable; import org.springframework.scheduling.TaskScheduler; /** @@ -74,12 +75,12 @@ public BatchingRabbitTemplate(ConnectionFactory connectionFactory, BatchingStrat } @Override - public synchronized void send(String exchange, String routingKey, Message message, CorrelationData correlationData) - throws AmqpException { + public synchronized void send(String exchange, String routingKey, Message message, + @Nullable CorrelationData correlationData) throws AmqpException { if (correlationData != null) { - if (logger.isDebugEnabled()) { - logger.debug("Cannot use batching with correlation data"); + if (this.logger.isDebugEnabled()) { + this.logger.debug("Cannot use batching with correlation data"); } super.send(exchange, routingKey, message, correlationData); } @@ -93,7 +94,7 @@ public synchronized void send(String exchange, String routingKey, Message messag } Date next = this.batchingStrategy.nextRelease(); if (next != null) { - this.scheduledTask = this.scheduler.schedule((Runnable) () -> releaseBatches(), next); + this.scheduledTask = this.scheduler.schedule((Runnable) () -> releaseBatches(), next.toInstant()); } } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java index a41fec74aa..27d26326b3 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 the original author or authors. + * Copyright 2016-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. @@ -18,11 +18,12 @@ import java.io.IOException; import java.lang.reflect.Constructor; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.Date; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; @@ -510,7 +511,7 @@ private void startMonitor(long idleEventInterval, final Map names } } processMonitorTask(); - }, this.monitorInterval); + }, Duration.ofMillis(this.monitorInterval)); } private void checkIdle(long idleEventInterval, long now) { @@ -578,7 +579,7 @@ private boolean restartConsumer(final Map namesToQueues, List Date: Wed, 20 Jul 2022 10:11:05 -0400 Subject: [PATCH 120/737] GH-1467: Support Spring AOT for Native Image Resolves https://github.com/spring-projects/spring-amqp/issues/1467 Migrate proxy hints from spring-native. Also add a proxy hint for `ChannelProxy` when it is also a `PublisherCallbackChannel`. --- .../aot/RabbitRuntimeHintsRegistrar.java | 53 +++++++++++++++++++ .../amqp/rabbit/aot/package-info.java | 6 +++ .../resources/META-INF/spring/aot.factories | 1 + 3 files changed, 60 insertions(+) create mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/RabbitRuntimeHintsRegistrar.java create mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/package-info.java create mode 100644 spring-rabbit/src/main/resources/META-INF/spring/aot.factories diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/RabbitRuntimeHintsRegistrar.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/RabbitRuntimeHintsRegistrar.java new file mode 100644 index 0000000000..f5c69b2a7d --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/RabbitRuntimeHintsRegistrar.java @@ -0,0 +1,53 @@ +/* + * 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.aot; + +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.connection.ChannelProxy; +import org.springframework.amqp.rabbit.connection.PublisherCallbackChannel; +import org.springframework.aop.SpringProxy; +import org.springframework.aop.framework.Advised; +import org.springframework.aot.hint.ProxyHints; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.TypeReference; +import org.springframework.aot.hint.support.RuntimeHintsUtils; +import org.springframework.core.DecoratingProxy; +import org.springframework.lang.Nullable; + +/** + * {@link RuntimeHintsRegistrar} for spring-rabbit. + * + * @author Gary Russell + * @since 3.0 + * + */ +public class RabbitRuntimeHintsRegistrar implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { + RuntimeHintsUtils.registerAnnotation(hints, RabbitListener.class); + ProxyHints proxyHints = hints.proxies(); + proxyHints.registerJdkProxy(ChannelProxy.class); + proxyHints.registerJdkProxy(ChannelProxy.class, PublisherCallbackChannel.class); + proxyHints.registerJdkProxy(builder -> + builder.proxiedInterfaces(TypeReference.of( + "org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer$ContainerDelegate")) + .proxiedInterfaces(SpringProxy.class, Advised.class, DecoratingProxy.class)); + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/package-info.java new file mode 100644 index 0000000000..7cf34f813e --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/package-info.java @@ -0,0 +1,6 @@ +/** + * Provides classes to support Spring AOT. + */ +@org.springframework.lang.NonNullApi +@org.springframework.lang.NonNullFields +package org.springframework.amqp.rabbit.aot; diff --git a/spring-rabbit/src/main/resources/META-INF/spring/aot.factories b/spring-rabbit/src/main/resources/META-INF/spring/aot.factories new file mode 100644 index 0000000000..77cfb34d6c --- /dev/null +++ b/spring-rabbit/src/main/resources/META-INF/spring/aot.factories @@ -0,0 +1 @@ +org.springframework.aot.hint.RuntimeHintsRegistrar=org.springframework.amqp.rabbit.aot.RabbitRuntimeHintsRegistrar From 18e4209f57b89cdc7ca645d1b27e8a5c04c88338 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 25 Jul 2022 10:46:32 -0400 Subject: [PATCH 121/737] GH-1474: Fix BatchingStrategy Propagation See https://github.com/spring-projects/spring-amqp/issues/1474 (Does not resolve because 2 issues are reported there). `BatchingStrategy` was not set by container factory. **cherry-pick to 2.4.x** --- .../AbstractRabbitListenerContainerFactory.java | 5 +++-- .../listener/AbstractRabbitListenerEndpoint.java | 3 ++- .../rabbit/listener/RabbitListenerEndpoint.java | 14 +++++++++++--- .../RabbitListenerContainerFactoryTests.java | 12 +++++++++++- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java index 69d6874173..bb1f6b1ffb 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java @@ -369,7 +369,8 @@ public C createListenerContainer(RabbitListenerEndpoint endpoint) { .acceptIfNotNull(this.phase, instance::setPhase) .acceptIfNotNull(this.afterReceivePostProcessors, instance::setAfterReceivePostProcessors) .acceptIfNotNull(this.deBatchingEnabled, instance::setDeBatchingEnabled) - .acceptIfNotNull(this.messageAckListener, instance::setMessageAckListener); + .acceptIfNotNull(this.messageAckListener, instance::setMessageAckListener) + .acceptIfNotNull(this.batchingStrategy, instance::setBatchingStrategy); if (this.batchListener && this.deBatchingEnabled == null) { // turn off container debatching by default for batch listeners instance.setDeBatchingEnabled(false); @@ -378,7 +379,7 @@ public C createListenerContainer(RabbitListenerEndpoint endpoint) { javaUtils .acceptIfNotNull(endpoint.getTaskExecutor(), instance::setTaskExecutor) .acceptIfNotNull(endpoint.getAckMode(), instance::setAcknowledgeMode) - .acceptIfNotNull(this.batchingStrategy, endpoint::setBatchingStrategy); + .acceptIfNotNull(endpoint.getBatchingStrategy(), instance::setBatchingStrategy); instance.setListenerId(endpoint.getId()); endpoint.setBatchListener(this.batchListener); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java index bcd7dffc4f..6aaaf7c700 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 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. @@ -308,6 +308,7 @@ public void setBatchListener(boolean batchListener) { this.batchListener = batchListener; } + @Override @Nullable public BatchingStrategy getBatchingStrategy() { return this.batchingStrategy; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpoint.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpoint.java index 979dd9ee1c..787a9473d5 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpoint.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -114,7 +114,6 @@ default TaskExecutor getTaskExecutor() { * @since 2.2 */ default void setBatchListener(boolean batchListener) { - // NOSONAR empty } /** @@ -124,7 +123,16 @@ default void setBatchListener(boolean batchListener) { * @see #setBatchListener(boolean) */ default void setBatchingStrategy(BatchingStrategy batchingStrategy) { - // NOSONAR empty + } + + /** + * Return this endpoint's batching strategy, or null. + * @return the strategy. + * @since 2.4.7 + */ + @Nullable + default BatchingStrategy getBatchingStrategy() { + return null; } /** diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerFactoryTests.java index 9707b14942..efcc268de8 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -28,6 +28,7 @@ import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.MessagePostProcessor; +import org.springframework.amqp.rabbit.batch.BatchingStrategy; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; import org.springframework.amqp.rabbit.listener.DirectMessageListenerContainer; @@ -35,6 +36,7 @@ import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.amqp.support.converter.SimpleMessageConverter; +import org.springframework.amqp.utils.test.TestUtils; import org.springframework.beans.DirectFieldAccessor; import org.springframework.scheduling.TaskScheduler; import org.springframework.transaction.PlatformTransactionManager; @@ -70,12 +72,17 @@ public void createSimpleContainer() { SimpleRabbitListenerEndpoint endpoint = new SimpleRabbitListenerEndpoint(); endpoint.setMessageListener(this.messageListener); endpoint.setQueueNames("myQueue"); + BatchingStrategy bs1 = mock(BatchingStrategy.class); + this.factory.setBatchingStrategy(bs1); + BatchingStrategy bs2 = mock(BatchingStrategy.class); + endpoint.setBatchingStrategy(bs2); SimpleMessageListenerContainer container = this.factory.createListenerContainer(endpoint); assertBasicConfig(container); assertThat(container.getMessageListener()).isEqualTo(messageListener); assertThat(container.getQueueNames()[0]).isEqualTo("myQueue"); + assertThat(TestUtils.getPropertyValue(container, "batchingStrategy")).isSameAs(bs2); } @Test @@ -89,6 +96,8 @@ public void createContainerFullConfig() { this.factory.setTaskExecutor(executor); this.factory.setTransactionManager(transactionManager); this.factory.setBatchSize(10); + BatchingStrategy bs1 = mock(BatchingStrategy.class); + this.factory.setBatchingStrategy(bs1); this.factory.setConcurrentConsumers(2); this.factory.setMaxConcurrentConsumers(5); this.factory.setStartConsumerMinInterval(2000L); @@ -115,6 +124,7 @@ public void createContainerFullConfig() { SimpleMessageListenerContainer container = this.factory.createListenerContainer(endpoint); assertBasicConfig(container); + assertThat(TestUtils.getPropertyValue(container, "batchingStrategy")).isSameAs(bs1); DirectFieldAccessor fieldAccessor = new DirectFieldAccessor(container); assertThat(fieldAccessor.getPropertyValue("taskExecutor")).isSameAs(executor); assertThat(fieldAccessor.getPropertyValue("transactionManager")).isSameAs(transactionManager); From f5aba94539287db35d572fe0f31457b99a9997b4 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 25 Jul 2022 11:41:21 -0400 Subject: [PATCH 122/737] GH-1474: Fix MessageProperties.lastInBatch Resolves https://github.com/spring-projects/spring-amqp/issues/1474 When consuming the whole debatched batch as a list, all messages had the `lastInBatch` property set to true. Clone the message properties for the last record. **cherry-pick to 2.4.x** --- .../amqp/core/MessageProperties.java | 11 ++++++++++- .../rabbit/batch/SimpleBatchingStrategy.java | 17 ++++++++++++----- .../core/BatchingRabbitTemplateTests.java | 6 +++--- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java index 42e92054ca..6f955d381e 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -140,6 +140,15 @@ public void setHeader(String key, Object value) { this.headers.put(key, value); } + /** + * Set headers. + * @param headers the headers. + * @since 2.4.7 + */ + public void setHeaders(Map headers) { + this.headers.putAll(headers); + } + /** * Typed getter for a header. * @param headerName the header name. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/SimpleBatchingStrategy.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/SimpleBatchingStrategy.java index 504fe548a7..d1558f7a10 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/SimpleBatchingStrategy.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/SimpleBatchingStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 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. @@ -29,6 +29,7 @@ import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; import org.springframework.amqp.support.AmqpHeaders; import org.springframework.amqp.support.converter.MessageConversionException; +import org.springframework.beans.BeanUtils; import org.springframework.util.Assert; /** @@ -184,10 +185,16 @@ public void deBatch(Message message, Consumer fragmentConsumer) { byte[] body = new byte[length]; byteBuffer.get(body); messageProperties.setContentLength(length); - // Caveat - shared MessageProperties. - Message fragment = new Message(body, messageProperties); - if (!byteBuffer.hasRemaining()) { - messageProperties.setLastInBatch(true); + // Caveat - shared MessageProperties, except for last + Message fragment; + if (byteBuffer.hasRemaining()) { + fragment = new Message(body, messageProperties); + } + else { + MessageProperties lastProperties = new MessageProperties(); + BeanUtils.copyProperties(messageProperties, lastProperties); + lastProperties.setLastInBatch(true); + fragment = new Message(body, lastProperties); } fragmentConsumer.accept(fragment); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplateTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplateTests.java index 60138e9045..e623ed0460 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplateTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 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. @@ -261,8 +261,8 @@ private void testDebatchByContainer(AbstractMessageListenerContainer container, if (asList) { container.setMessageListener((BatchMessageListener) messages -> { received.addAll(messages); - lastInBatch.add(false); - lastInBatch.add(true); + lastInBatch.add(messages.get(0).getMessageProperties().isLastInBatch()); + lastInBatch.add(messages.get(1).getMessageProperties().isLastInBatch()); batchSize.set(messages.size()); latch.countDown(); }); From 655f4604103bf429f73f33f67598d8a7eac79a76 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 27 Jul 2022 10:08:03 -0400 Subject: [PATCH 123/737] Upgrade Micrometer Versions --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index caeb838ee5..fdfecb3480 100644 --- a/build.gradle +++ b/build.gradle @@ -55,8 +55,8 @@ ext { log4jVersion = '2.17.2' logbackVersion = '1.2.3' lz4Version = '1.8.0' - micrometerVersion = '1.10.0-M2' - micrometerTracingVersion = '1.0.0-M5' + micrometerVersion = '1.10.0-M3' + micrometerTracingVersion = '1.0.0-M6' mockitoVersion = '4.5.1' rabbitmqStreamVersion = '0.4.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.13.1' From 330764c28c11db4b0c3ab1ec745f76b214ba2f5a Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 27 Jul 2022 10:20:45 -0400 Subject: [PATCH 124/737] Fix Micrometer Tracing Artifact Name --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index fdfecb3480..f2f2310386 100644 --- a/build.gradle +++ b/build.gradle @@ -387,7 +387,7 @@ project('spring-rabbit') { optionalApi "ch.qos.logback:logback-classic:$logbackVersion" optionalApi 'org.apache.logging.log4j:log4j-core' optionalApi 'io.micrometer:micrometer-core' - optionalApi 'io.micrometer:micrometer-tracing-api' + optionalApi 'io.micrometer:micrometer-tracing' // Spring Data projection message binding support optionalApi ("org.springframework.data:spring-data-commons") { exclude group: 'org.springframework' From 8becb8925e5e209987fcfb35d5f59eac97847d56 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 28 Jul 2022 10:41:27 -0400 Subject: [PATCH 125/737] Fix Sonar Issue --- .../amqp/rabbit/connection/ThreadChannelConnectionFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java index 69edbc6456..0eb4593401 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java @@ -119,7 +119,7 @@ public void addConnectionListener(ConnectionListener listener) { public synchronized Connection createConnection() throws AmqpException { if (this.connection == null || !this.connection.isOpen()) { Connection bareConnection = createBareConnection(); // NOSONAR - see destroy() - this.connection = new ConnectionWrapper(bareConnection.getDelegate(), getCloseTimeout()); + this.connection = new ConnectionWrapper(bareConnection.getDelegate(), getCloseTimeout()); // NOSONAR getConnectionListener().onCreate(this.connection); } return this.connection; From b03a0bc9afedd9a3398df1ea0c97e887c69b3c7a Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 28 Jul 2022 16:08:58 -0400 Subject: [PATCH 126/737] GH-1473: Switch to CompletableFuture Resolves https://github.com/spring-projects/spring-amqp/issues/1473 * Deprecate `AsyncRabbitTemplate2`. * Copy implementation from `AsyncRabbitTemplate2` to `AsyncRabbitTemplate` * Fix other `ListenableFuture` usages. --- .../amqp/core/AsyncAmqpTemplate.java | 67 +++---- .../amqp/rabbit/AsyncRabbitTemplate.java | 184 ++++-------------- .../amqp/rabbit/AsyncRabbitTemplate2.java | 64 ++++++ .../amqp/rabbit/RabbitConverterFuture.java | 54 +++++ .../amqp/rabbit/RabbitFuture.java | 116 +++++++++++ .../amqp/rabbit/RabbitMessageFuture.java | 40 ++++ .../amqp/rabbit/TimeoutTask.java | 59 ++++++ .../rabbit/connection/CorrelationData.java | 19 +- .../PublisherCallbackChannelImpl.java | 6 +- .../AbstractAdaptableMessageListener.java | 17 +- .../adapter/DelegatingInvocableHandler.java | 6 +- .../listener/adapter/HandlerAdapter.java | 6 +- ...RepublishMessageRecovererWithConfirms.java | 3 +- .../amqp/rabbit/AsyncRabbitTemplateTests.java | 148 +++++++------- .../rabbit/annotation/AsyncListenerTests.java | 41 ++-- .../ComplexTypeJsonIntegrationTests.java | 4 +- .../core/MessagingTemplateConfirmsTests.java | 2 +- ...tePublisherCallbacksIntegrationTests2.java | 2 +- .../rabbit/listener/AsyncReplyToTests.java | 17 +- .../adapter/MessageListenerAdapterTests.java | 13 +- src/reference/asciidoc/amqp.adoc | 32 ++- src/reference/asciidoc/appendix.adoc | 10 + src/reference/asciidoc/whats-new.adoc | 6 + 23 files changed, 592 insertions(+), 324 deletions(-) create mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate2.java create mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitConverterFuture.java create mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitFuture.java create mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitMessageFuture.java create mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/TimeoutTask.java diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate.java index cd84b043f1..123c3b6fa5 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2020-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. @@ -16,12 +16,13 @@ package org.springframework.amqp.core; +import java.util.concurrent.CompletableFuture; + import org.springframework.core.ParameterizedTypeReference; -import org.springframework.util.concurrent.ListenableFuture; /** * Classes implementing this interface can perform asynchronous send and - * receive operations. + * receive operations using {@link CompletableFuture}s. * * @author Gary Russell * @since 2.0 @@ -33,18 +34,18 @@ public interface AsyncAmqpTemplate { * Send a message to the default exchange with the default routing key. If the message * contains a correlationId property, it must be unique. * @param message the message. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture sendAndReceive(Message message); + CompletableFuture sendAndReceive(Message message); /** * Send a message to the default exchange with the supplied routing key. If the message * contains a correlationId property, it must be unique. * @param routingKey the routing key. * @param message the message. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture sendAndReceive(String routingKey, Message message); + CompletableFuture sendAndReceive(String routingKey, Message message); /** * Send a message to the supplied exchange and routing key. If the message @@ -52,18 +53,18 @@ public interface AsyncAmqpTemplate { * @param exchange the exchange. * @param routingKey the routing key. * @param message the message. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture sendAndReceive(String exchange, String routingKey, Message message); + CompletableFuture sendAndReceive(String exchange, String routingKey, Message message); /** * Convert the object to a message and send it to the default exchange with the * default routing key. * @param object the object to convert. * @param the expected result type. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture convertSendAndReceive(Object object); + CompletableFuture convertSendAndReceive(Object object); /** * Convert the object to a message and send it to the default exchange with the @@ -71,9 +72,9 @@ public interface AsyncAmqpTemplate { * @param routingKey the routing key. * @param object the object to convert. * @param the expected result type. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture convertSendAndReceive(String routingKey, Object object); + CompletableFuture convertSendAndReceive(String routingKey, Object object); /** * Convert the object to a message and send it to the provided exchange and @@ -82,9 +83,9 @@ public interface AsyncAmqpTemplate { * @param routingKey the routing key. * @param object the object to convert. * @param the expected result type. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture convertSendAndReceive(String exchange, String routingKey, Object object); + CompletableFuture convertSendAndReceive(String exchange, String routingKey, Object object); /** * Convert the object to a message and send it to the default exchange with the @@ -93,9 +94,9 @@ public interface AsyncAmqpTemplate { * @param object the object to convert. * @param messagePostProcessor the post processor. * @param the expected result type. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture convertSendAndReceive(Object object, MessagePostProcessor messagePostProcessor); + CompletableFuture convertSendAndReceive(Object object, MessagePostProcessor messagePostProcessor); /** * Convert the object to a message and send it to the default exchange with the @@ -105,9 +106,9 @@ public interface AsyncAmqpTemplate { * @param object the object to convert. * @param messagePostProcessor the post processor. * @param the expected result type. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture convertSendAndReceive(String routingKey, Object object, + CompletableFuture convertSendAndReceive(String routingKey, Object object, MessagePostProcessor messagePostProcessor); /** @@ -119,9 +120,9 @@ ListenableFuture convertSendAndReceive(String routingKey, Object object, * @param object the object to convert. * @param messagePostProcessor the post processor. * @param the expected result type. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture convertSendAndReceive(String exchange, String routingKey, Object object, + CompletableFuture convertSendAndReceive(String exchange, String routingKey, Object object, MessagePostProcessor messagePostProcessor); /** @@ -130,9 +131,9 @@ ListenableFuture convertSendAndReceive(String exchange, String routingKey * @param object the object to convert. * @param responseType the response type. * @param the expected result type. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture convertSendAndReceiveAsType(Object object, ParameterizedTypeReference responseType); + CompletableFuture convertSendAndReceiveAsType(Object object, ParameterizedTypeReference responseType); /** * Convert the object to a message and send it to the default exchange with the @@ -141,9 +142,9 @@ ListenableFuture convertSendAndReceive(String exchange, String routingKey * @param object the object to convert. * @param responseType the response type. * @param the expected result type. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture convertSendAndReceiveAsType(String routingKey, Object object, + CompletableFuture convertSendAndReceiveAsType(String routingKey, Object object, ParameterizedTypeReference responseType); /** @@ -154,9 +155,9 @@ ListenableFuture convertSendAndReceiveAsType(String routingKey, Object ob * @param object the object to convert. * @param responseType the response type. * @param the expected result type. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture convertSendAndReceiveAsType(String exchange, String routingKey, Object object, + CompletableFuture convertSendAndReceiveAsType(String exchange, String routingKey, Object object, ParameterizedTypeReference responseType); /** @@ -167,9 +168,9 @@ ListenableFuture convertSendAndReceiveAsType(String exchange, String rout * @param messagePostProcessor the post processor. * @param responseType the response type. * @param the expected result type. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture convertSendAndReceiveAsType(Object object, MessagePostProcessor messagePostProcessor, + CompletableFuture convertSendAndReceiveAsType(Object object, MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType); /** @@ -181,9 +182,9 @@ ListenableFuture convertSendAndReceiveAsType(Object object, MessagePostPr * @param messagePostProcessor the post processor. * @param responseType the response type. * @param the expected result type. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture convertSendAndReceiveAsType(String routingKey, Object object, + CompletableFuture convertSendAndReceiveAsType(String routingKey, Object object, MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType); /** @@ -196,9 +197,9 @@ ListenableFuture convertSendAndReceiveAsType(String routingKey, Object ob * @param messagePostProcessor the post processor. * @param responseType the response type. * @param the expected result type. - * @return the {@link ListenableFuture}. + * @return the {@link CompletableFuture}. */ - ListenableFuture convertSendAndReceiveAsType(String exchange, String routingKey, Object object, + CompletableFuture convertSendAndReceiveAsType(String exchange, String routingKey, Object object, MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java index 3d31b4cd65..56dcfe9b4a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 the original author or authors. + * 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. @@ -18,6 +18,7 @@ import java.time.Instant; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ScheduledFuture; @@ -29,7 +30,6 @@ import org.springframework.amqp.AmqpIllegalStateException; import org.springframework.amqp.core.Address; import org.springframework.amqp.core.AmqpMessageReturnedException; -import org.springframework.amqp.core.AmqpReplyTimeoutException; import org.springframework.amqp.core.AsyncAmqpTemplate; import org.springframework.amqp.core.Correlation; import org.springframework.amqp.core.Message; @@ -60,17 +60,15 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.util.Assert; import org.springframework.util.StringUtils; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.SettableListenableFuture; import com.rabbitmq.client.Channel; /** - * Provides asynchronous send and receive operations returning a {@link ListenableFuture} + * Provides asynchronous send and receive operations returning a {@link CompletableFuture} * allowing the caller to obtain the reply later, using {@code get()} or a callback. *

* When confirms are enabled, the future has a confirm property which is itself a - * {@link ListenableFuture}. If the reply is received before the publisher confirm, + * {@link CompletableFuture}. If the reply is received before the publisher confirm, * the confirm is discarded since the reply implicitly indicates the message was * published. *

@@ -296,7 +294,7 @@ public void setMandatoryExpressionString(String mandatoryExpression) { /** * Set to true to enable publisher confirms. When enabled, the {@link RabbitFuture} * returned by the send and receive operation will have a - * {@code ListenableFuture} in its {@code confirm} property. + * {@code CompletableFuture} in its {@code confirm} property. * @param enableConfirms true to enable publisher confirms. */ public void setEnableConfirms(boolean enableConfirms) { @@ -375,11 +373,12 @@ public RabbitMessageFuture sendAndReceive(String routingKey, Message message) { @Override public RabbitMessageFuture sendAndReceive(String exchange, String routingKey, Message message) { String correlationId = getOrSetCorrelationIdAndSetReplyTo(message, null); - RabbitMessageFuture future = new RabbitMessageFuture(correlationId, message); + RabbitMessageFuture future = new RabbitMessageFuture(correlationId, message, this::canceler, + this::timeoutTask); CorrelationData correlationData = null; if (this.enableConfirms) { correlationData = new CorrelationData(correlationId); - future.setConfirm(new SettableListenableFuture<>()); + future.setConfirm(new CompletableFuture<>()); } this.pending.put(correlationId, future); if (this.container != null) { @@ -578,7 +577,7 @@ public void onMessage(Message message, Channel channel) { } RabbitFuture future = this.pending.remove(correlationId); if (future != null) { - if (future instanceof AsyncRabbitTemplate.RabbitConverterFuture) { + if (future instanceof RabbitConverterFuture) { MessageConverter messageConverter = this.template.getMessageConverter(); RabbitConverterFuture rabbitFuture = (RabbitConverterFuture) future; Object converted = rabbitFuture.getReturnType() != null @@ -586,10 +585,10 @@ public void onMessage(Message message, Channel channel) { ? ((SmartMessageConverter) messageConverter).fromMessage(message, rabbitFuture.getReturnType()) : messageConverter.fromMessage(message); - rabbitFuture.set(converted); + rabbitFuture.complete(converted); } else { - ((RabbitMessageFuture) future).set(message); + ((RabbitMessageFuture) future).complete(message); } } else { @@ -608,7 +607,7 @@ public void returnedMessage(ReturnedMessage returned) { if (StringUtils.hasText(correlationId)) { RabbitFuture future = this.pending.remove(correlationId); if (future != null) { - future.setException(new AmqpMessageReturnedException("Message returned", returned)); + future.completeExceptionally(new AmqpMessageReturnedException("Message returned", returned)); } else { if (this.logger.isWarnEnabled()) { @@ -630,7 +629,7 @@ public void confirm(@NonNull CorrelationData correlationData, boolean ack, @Null RabbitFuture future = this.pending.get(correlationId); if (future != null) { future.setNackCause(cause); - ((SettableListenableFuture) future.getConfirm()).set(ack); + future.getConfirm().complete(ack); } else { if (this.logger.isDebugEnabled()) { @@ -661,145 +660,33 @@ private String getOrSetCorrelationIdAndSetReplyTo(Message message, return correlationId; } - @Override - public String toString() { - return this.beanName == null ? super.toString() : (this.getClass().getSimpleName() + ": " + this.beanName); - } - - /** - * Base class for {@link ListenableFuture}s returned by {@link AsyncRabbitTemplate}. - * @param the type. - * @since 1.6 - */ - public abstract class RabbitFuture extends SettableListenableFuture { - - private final String correlationId; - - private final Message requestMessage; - - private ScheduledFuture timeoutTask; - - private volatile ListenableFuture confirm; - - private String nackCause; - - private ChannelHolder channelHolder; - - public RabbitFuture(String correlationId, Message requestMessage) { - this.correlationId = correlationId; - this.requestMessage = requestMessage; - } - - void setChannelHolder(ChannelHolder channel) { - this.channelHolder = channel; + private void canceler(String correlationId, @Nullable ChannelHolder channelHolder) { + this.pending.remove(correlationId); + if (channelHolder != null && this.directReplyToContainer != null) { + this.directReplyToContainer + .releaseConsumerFor(channelHolder, false, null); // NOSONAR } + } - @Override - public boolean cancel(boolean mayInterruptIfRunning) { - if (this.timeoutTask != null) { - this.timeoutTask.cancel(true); - } - AsyncRabbitTemplate.this.pending.remove(this.correlationId); - if (this.channelHolder != null && AsyncRabbitTemplate.this.directReplyToContainer != null) { - AsyncRabbitTemplate.this.directReplyToContainer - .releaseConsumerFor(this.channelHolder, false, null); // NOSONAR - } - return super.cancel(mayInterruptIfRunning); - } - - /** - * When confirms are enabled contains a {@link ListenableFuture} - * for the confirmation. - * @return the future. - */ - public ListenableFuture getConfirm() { - return this.confirm; - } - - void setConfirm(ListenableFuture confirm) { - this.confirm = confirm; - } - - /** - * When confirms are enabled and a nack is received, contains - * the cause for the nack, if any. - * @return the cause. - */ - public String getNackCause() { - return this.nackCause; - } - - void setNackCause(String nackCause) { - this.nackCause = nackCause; - } - - void startTimer() { - if (AsyncRabbitTemplate.this.receiveTimeout > 0) { - synchronized (AsyncRabbitTemplate.this) { - if (!AsyncRabbitTemplate.this.running) { - AsyncRabbitTemplate.this.pending.remove(this.correlationId); - throw new IllegalStateException("'AsyncRabbitTemplate' must be started."); - } - this.timeoutTask = AsyncRabbitTemplate.this.taskScheduler.schedule(new TimeoutTask(), - Instant.now().plusMillis(AsyncRabbitTemplate.this.receiveTimeout)); - } - } - else { - this.timeoutTask = null; - } - } - - private class TimeoutTask implements Runnable { - - @Override - public void run() { - AsyncRabbitTemplate.this.pending.remove(RabbitFuture.this.correlationId); - if (RabbitFuture.this.channelHolder != null - && AsyncRabbitTemplate.this.directReplyToContainer != null) { - AsyncRabbitTemplate.this.directReplyToContainer - .releaseConsumerFor(RabbitFuture.this.channelHolder, false, null); // NOSONAR + @Nullable + private ScheduledFuture timeoutTask(RabbitFuture future) { + if (this.receiveTimeout > 0) { + synchronized (this) { + if (!this.running) { + this.pending.remove(future.getCorrelationId()); + throw new IllegalStateException("'AsyncRabbitTemplate' must be started."); } - setException(new AmqpReplyTimeoutException("Reply timed out", RabbitFuture.this.requestMessage)); + return this.taskScheduler.schedule( + new TimeoutTask(future, this.pending, this.directReplyToContainer), + Instant.now().plusMillis(this.receiveTimeout)); } - - } - - } - - /** - * A {@link RabbitFuture} with a return type of {@link Message}. - * @since 1.6 - */ - public class RabbitMessageFuture extends RabbitFuture { - - public RabbitMessageFuture(String correlationId, Message requestMessage) { - super(correlationId, requestMessage); } - + return null; } - /** - * A {@link RabbitFuture} with a return type of the template's - * generic parameter. - * @param the type. - * @since 1.6 - */ - public class RabbitConverterFuture extends RabbitFuture { - - private volatile ParameterizedTypeReference returnType; - - public RabbitConverterFuture(String correlationId, Message requestMessage) { - super(correlationId, requestMessage); - } - - public ParameterizedTypeReference getReturnType() { - return this.returnType; - } - - public void setReturnType(ParameterizedTypeReference returnType) { - this.returnType = returnType; - } - + @Override + public String toString() { + return this.beanName == null ? super.toString() : (this.getClass().getSimpleName() + ": " + this.beanName); } private final class CorrelationMessagePostProcessor implements MessagePostProcessor { @@ -821,10 +708,11 @@ public Message postProcessMessage(Message message, Correlation correlation) thro messageToSend = correlationData.userPostProcessor.postProcessMessage(message); } String correlationId = getOrSetCorrelationIdAndSetReplyTo(messageToSend, correlationData); - correlationData.future = new RabbitConverterFuture(correlationId, message); + correlationData.future = new RabbitConverterFuture(correlationId, message, + AsyncRabbitTemplate.this::canceler, AsyncRabbitTemplate.this::timeoutTask); if (correlationData.enableConfirms) { correlationData.setId(correlationId); - correlationData.future.setConfirm(new SettableListenableFuture<>()); + correlationData.future.setConfirm(new CompletableFuture<>()); } correlationData.future.setReturnType(correlationData.returnType); AsyncRabbitTemplate.this.pending.put(correlationId, correlationData.future); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate2.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate2.java new file mode 100644 index 0000000000..a02af02c99 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate2.java @@ -0,0 +1,64 @@ +/* + * 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; + +import java.util.concurrent.CompletableFuture; + +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; + +/** + * This class was added in 2.4.7 to aid migration from methods returning + * {@code ListenableFuture}s to {@link CompletableFuture}s. + * + * @author Gary Russell + * @since 2.4.7 + * @deprecated in favor of {@link AsyncRabbitTemplate}. + * + */ +@Deprecated +public class AsyncRabbitTemplate2 extends AsyncRabbitTemplate { + + public AsyncRabbitTemplate2(ConnectionFactory connectionFactory, String exchange, String routingKey, + String replyQueue, String replyAddress) { + super(connectionFactory, exchange, routingKey, replyQueue, replyAddress); + } + + public AsyncRabbitTemplate2(ConnectionFactory connectionFactory, String exchange, String routingKey, + String replyQueue) { + super(connectionFactory, exchange, routingKey, replyQueue); + } + + public AsyncRabbitTemplate2(ConnectionFactory connectionFactory, String exchange, String routingKey) { + super(connectionFactory, exchange, routingKey); + } + + public AsyncRabbitTemplate2(RabbitTemplate template, AbstractMessageListenerContainer container, + String replyAddress) { + super(template, container, replyAddress); + } + + public AsyncRabbitTemplate2(RabbitTemplate template, AbstractMessageListenerContainer container) { + super(template, container); + } + + public AsyncRabbitTemplate2(RabbitTemplate template) { + super(template); + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitConverterFuture.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitConverterFuture.java new file mode 100644 index 0000000000..fe799c577a --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitConverterFuture.java @@ -0,0 +1,54 @@ +/* + * 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; + +import java.util.concurrent.ScheduledFuture; +import java.util.function.BiConsumer; +import java.util.function.Function; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.rabbit.listener.DirectReplyToMessageListenerContainer.ChannelHolder; +import org.springframework.core.ParameterizedTypeReference; + +/** + * A {@link RabbitFuture} with a return type of the template's + * generic parameter. + * @param the type. + * + * @author Gary Russell + * @since 2.4.7 + */ +public class RabbitConverterFuture extends RabbitFuture { + + private volatile ParameterizedTypeReference returnType; + + RabbitConverterFuture(String correlationId, Message requestMessage, + BiConsumer canceler, + Function, ScheduledFuture> timeoutTaskFunction) { + + super(correlationId, requestMessage, canceler, timeoutTaskFunction); + } + + public ParameterizedTypeReference getReturnType() { + return this.returnType; + } + + public void setReturnType(ParameterizedTypeReference returnType) { + this.returnType = returnType; + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitFuture.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitFuture.java new file mode 100644 index 0000000000..17bab1884b --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitFuture.java @@ -0,0 +1,116 @@ +/* + * 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; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ScheduledFuture; +import java.util.function.BiConsumer; +import java.util.function.Function; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.rabbit.listener.DirectReplyToMessageListenerContainer.ChannelHolder; + +/** + * Base class for {@link CompletableFuture}s returned by {@link AsyncRabbitTemplate}. + * @param the type. + * + * @author Gary Russell + * @since 2.4.7 + */ +public abstract class RabbitFuture extends CompletableFuture { + + private final String correlationId; + + private final Message requestMessage; + + private final BiConsumer canceler; + + private final Function, ScheduledFuture> timeoutTaskFunction; + + private ScheduledFuture timeoutTask; + + private volatile CompletableFuture confirm; + + private String nackCause; + + private ChannelHolder channelHolder; + + protected RabbitFuture(String correlationId, Message requestMessage, BiConsumer canceler, + Function, ScheduledFuture> timeoutTaskFunction) { + + this.correlationId = correlationId; + this.requestMessage = requestMessage; + this.canceler = canceler; + this.timeoutTaskFunction = timeoutTaskFunction; + } + + void setChannelHolder(ChannelHolder channel) { + this.channelHolder = channel; + } + + String getCorrelationId() { + return this.correlationId; + } + + ChannelHolder getChannelHolder() { + return this.channelHolder; + } + + Message getRequestMessage() { + return this.requestMessage; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + if (this.timeoutTask != null) { + this.timeoutTask.cancel(true); + } + this.canceler.accept(this.correlationId, this.channelHolder); + return super.cancel(mayInterruptIfRunning); + } + + /** + * When confirms are enabled contains a {@link CompletableFuture} + * for the confirmation. + * @return the future. + */ + public CompletableFuture getConfirm() { + return this.confirm; + } + + void setConfirm(CompletableFuture confirm) { + this.confirm = confirm; + } + + /** + * When confirms are enabled and a nack is received, contains + * the cause for the nack, if any. + * @return the cause. + */ + public String getNackCause() { + return this.nackCause; + } + + void setNackCause(String nackCause) { + this.nackCause = nackCause; + } + + void startTimer() { + this.timeoutTask = this.timeoutTaskFunction.apply(this); + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitMessageFuture.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitMessageFuture.java new file mode 100644 index 0000000000..467053a35d --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitMessageFuture.java @@ -0,0 +1,40 @@ +/* + * 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; + +import java.util.concurrent.ScheduledFuture; +import java.util.function.BiConsumer; +import java.util.function.Function; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.rabbit.listener.DirectReplyToMessageListenerContainer.ChannelHolder; + +/** + * A {@link RabbitFuture} with a return type of {@link Message}. + * + * @author Gary Russell + * @since 2.4.7 + */ +public class RabbitMessageFuture extends RabbitFuture { + + RabbitMessageFuture(String correlationId, Message requestMessage, BiConsumer canceler, + Function, ScheduledFuture> timeoutTaskFunction) { + + super(correlationId, requestMessage, canceler, timeoutTaskFunction); + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/TimeoutTask.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/TimeoutTask.java new file mode 100644 index 0000000000..8ca3af1667 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/TimeoutTask.java @@ -0,0 +1,59 @@ +/* + * 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; + +import java.util.concurrent.ConcurrentMap; + +import org.springframework.amqp.core.AmqpReplyTimeoutException; +import org.springframework.amqp.rabbit.listener.DirectReplyToMessageListenerContainer; +import org.springframework.amqp.rabbit.listener.DirectReplyToMessageListenerContainer.ChannelHolder; +import org.springframework.lang.Nullable; + +/** + * A {@link Runnable} used to time out a {@link RabbitFuture}. + * + * @author Gary Russell + * @since 2.4.7 + */ +public class TimeoutTask implements Runnable { + + private final RabbitFuture future; + + private final ConcurrentMap> pending; + + private final DirectReplyToMessageListenerContainer container; + + TimeoutTask(RabbitFuture future, ConcurrentMap> pending, + @Nullable DirectReplyToMessageListenerContainer container) { + + this.future = future; + this.pending = pending; + this.container = container; + } + + @Override + public void run() { + this.pending.remove(this.future.getCorrelationId()); + ChannelHolder holder = this.future.getChannelHolder(); + if (holder != null && this.container != null) { + this.container.releaseConsumerFor(holder, false, null); // NOSONAR + } + this.future.completeExceptionally( + new AmqpReplyTimeoutException("Reply timed out", this.future.getRequestMessage())); + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java index 14162a82d8..46ffaedcd7 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -17,13 +17,13 @@ package org.springframework.amqp.rabbit.connection; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import org.springframework.amqp.core.Correlation; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.ReturnedMessage; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.concurrent.SettableListenableFuture; /** * Base class for correlating publisher confirms to sent messages. Use the @@ -41,7 +41,7 @@ */ public class CorrelationData implements Correlation { - private final SettableListenableFuture future = new SettableListenableFuture<>(); + private final CompletableFuture future = new CompletableFuture<>(); private volatile String id; @@ -91,7 +91,18 @@ public void setId(String id) { * @return the future. * @since 2.1 */ - public SettableListenableFuture getFuture() { + public CompletableFuture getFuture() { + return this.future; + } + + /** + * Return a future to check the success/failure of the publish operation. + * @return the future. + * @since 2.4.7 + * @deprecated in favor of {@link #getFuture()}. + */ + @Deprecated + public CompletableFuture getCompletableFuture() { return this.future; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java index 01f710b2aa..d1c695d9e8 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -955,7 +955,7 @@ private void doProcessAck(long seq, boolean ack, boolean multiple, boolean remov if (pendingConfirm != null) { CorrelationData correlationData = pendingConfirm.getCorrelationData(); if (correlationData != null) { - correlationData.getFuture().set(new Confirm(ack, pendingConfirm.getCause())); + correlationData.getFuture().complete(new Confirm(ack, pendingConfirm.getCause())); if (StringUtils.hasText(correlationData.getId())) { this.pendingReturns.remove(correlationData.getId()); // NOSONAR never null } @@ -991,7 +991,7 @@ private void processMultipleAck(long seq, boolean ack) { PendingConfirm value = entry.getValue(); CorrelationData correlationData = value.getCorrelationData(); if (correlationData != null) { - correlationData.getFuture().set(new Confirm(ack, value.getCause())); + correlationData.getFuture().complete(new Confirm(ack, value.getCause())); if (StringUtils.hasText(correlationData.getId())) { this.pendingReturns.remove(correlationData.getId()); // NOSONAR never null } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java index 8746f17767..2e0bd8181e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 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. @@ -21,6 +21,7 @@ import java.lang.reflect.Type; import java.lang.reflect.WildcardType; import java.util.Arrays; +import java.util.concurrent.CompletableFuture; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -52,7 +53,6 @@ import org.springframework.retry.support.RetryTemplate; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import org.springframework.util.concurrent.ListenableFuture; import com.rabbitmq.client.Channel; @@ -374,17 +374,20 @@ protected void handleResult(InvocationResult resultArg, Message request, Channel */ protected void handleResult(InvocationResult resultArg, Message request, Channel channel, Object source) { if (channel != null) { - if (resultArg.getReturnValue() instanceof ListenableFuture) { + if (resultArg.getReturnValue() instanceof CompletableFuture) { if (!this.isManualAck) { this.logger.warn("Container AcknowledgeMode must be MANUAL for a Future return type; " + "otherwise the container will ack the message immediately"); } - ((ListenableFuture) resultArg.getReturnValue()).addCallback( - r -> { + ((CompletableFuture) resultArg.getReturnValue()).whenComplete((r, t) -> { + if (t == null) { asyncSuccess(resultArg, request, channel, source, r); basicAck(request, channel); - }, - t -> asyncFailure(request, channel, t)); + } + else { + asyncFailure(request, channel, t); + } + }); } else if (monoPresent && MonoHandler.isMono(resultArg.getReturnValue())) { if (!this.isManualAck) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandler.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandler.java index 96bb30fc36..6a99f75a2b 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandler.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2021 the original author or authors. + * Copyright 2015-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. @@ -23,6 +23,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -45,7 +46,6 @@ import org.springframework.messaging.handler.annotation.support.PayloadMethodArgumentResolver; import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; import org.springframework.util.Assert; -import org.springframework.util.concurrent.ListenableFuture; import org.springframework.validation.Validator; @@ -147,7 +147,7 @@ public DelegatingInvocableHandler(List handlers, private boolean isAsyncReply(InvocableHandlerMethod method) { return (AbstractAdaptableMessageListener.monoPresent && MonoHandler.isMono(method.getMethod().getReturnType())) - || ListenableFuture.class.isAssignableFrom(method.getMethod().getReturnType()); + || CompletableFuture.class.isAssignableFrom(method.getMethod().getReturnType()); } /** diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/HandlerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/HandlerAdapter.java index 29d3bd45fd..e6e0f1a172 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/HandlerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/HandlerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2021 the original author or authors. + * Copyright 2015-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. @@ -18,11 +18,11 @@ import java.lang.reflect.Method; import java.lang.reflect.Type; +import java.util.concurrent.CompletableFuture; import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; -import org.springframework.util.concurrent.ListenableFuture; /** * A wrapper for either an {@link InvocableHandlerMethod} or @@ -50,7 +50,7 @@ public HandlerAdapter(InvocableHandlerMethod invokerHandlerMethod) { this.delegatingHandler = null; this.asyncReplies = (AbstractAdaptableMessageListener.monoPresent && MonoHandler.isMono(invokerHandlerMethod.getMethod().getReturnType())) - || ListenableFuture.class.isAssignableFrom(invokerHandlerMethod.getMethod().getReturnType()); + || CompletableFuture.class.isAssignableFrom(invokerHandlerMethod.getMethod().getReturnType()); } /** diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirms.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirms.java index 1c65f5931e..cbdd107bf2 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirms.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirms.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-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. @@ -108,6 +108,7 @@ protected void doSend(@Nullable } } + @SuppressWarnings("deprecation") private void doSendCorrelated(String exchange, String routingKey, Message message) { CorrelationData cd = new CorrelationData(); if (exchange != null) { diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java index 75eddf5016..039355ecea 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-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. @@ -19,15 +19,18 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; import static org.awaitility.Awaitility.await; +import static org.mockito.Mockito.mock; import java.util.Map; import java.util.UUID; import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; import org.junit.jupiter.api.Test; @@ -38,14 +41,13 @@ import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.core.Queue; -import org.springframework.amqp.rabbit.AsyncRabbitTemplate.RabbitConverterFuture; -import org.springframework.amqp.rabbit.AsyncRabbitTemplate.RabbitMessageFuture; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory.ConfirmType; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitAdmin; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.junit.RabbitAvailable; +import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter; import org.springframework.amqp.rabbit.listener.adapter.ReplyingMessageListener; @@ -59,8 +61,6 @@ import org.springframework.context.annotation.Primary; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.ListenableFutureCallback; /** * @author Gary Russell @@ -90,7 +90,7 @@ public class AsyncRabbitTemplateTests { @Test public void testConvert1Arg() throws Exception { final AtomicBoolean mppCalled = new AtomicBoolean(); - ListenableFuture future = this.asyncTemplate.convertSendAndReceive("foo", m -> { + CompletableFuture future = this.asyncTemplate.convertSendAndReceive("foo", m -> { mppCalled.set(true); return m; }); @@ -101,8 +101,8 @@ public void testConvert1Arg() throws Exception { @Test public void testConvert1ArgDirect() throws Exception { this.latch.set(new CountDownLatch(1)); - ListenableFuture future1 = this.asyncDirectTemplate.convertSendAndReceive("foo"); - ListenableFuture future2 = this.asyncDirectTemplate.convertSendAndReceive("bar"); + CompletableFuture future1 = this.asyncDirectTemplate.convertSendAndReceive("foo"); + CompletableFuture future2 = this.asyncDirectTemplate.convertSendAndReceive("bar"); this.latch.get().countDown(); checkConverterResult(future1, "FOO"); checkConverterResult(future2, "BAR"); @@ -127,19 +127,19 @@ public void testConvert1ArgDirect() throws Exception { @Test public void testConvert2Args() throws Exception { - ListenableFuture future = this.asyncTemplate.convertSendAndReceive(this.requests.getName(), "foo"); + CompletableFuture future = this.asyncTemplate.convertSendAndReceive(this.requests.getName(), "foo"); checkConverterResult(future, "FOO"); } @Test public void testConvert3Args() throws Exception { - ListenableFuture future = this.asyncTemplate.convertSendAndReceive("", this.requests.getName(), "foo"); + CompletableFuture future = this.asyncTemplate.convertSendAndReceive("", this.requests.getName(), "foo"); checkConverterResult(future, "FOO"); } @Test public void testConvert4Args() throws Exception { - ListenableFuture future = this.asyncTemplate.convertSendAndReceive("", this.requests.getName(), "foo", + CompletableFuture future = this.asyncTemplate.convertSendAndReceive("", this.requests.getName(), "foo", message -> { String body = new String(message.getBody()); return new Message((body + "bar").getBytes(), message.getMessageProperties()); @@ -149,15 +149,15 @@ public void testConvert4Args() throws Exception { @Test public void testMessage1Arg() throws Exception { - ListenableFuture future = this.asyncTemplate.sendAndReceive(getFooMessage()); + CompletableFuture future = this.asyncTemplate.sendAndReceive(getFooMessage()); checkMessageResult(future, "FOO"); } @Test public void testMessage1ArgDirect() throws Exception { this.latch.set(new CountDownLatch(1)); - ListenableFuture future1 = this.asyncDirectTemplate.sendAndReceive(getFooMessage()); - ListenableFuture future2 = this.asyncDirectTemplate.sendAndReceive(getFooMessage()); + CompletableFuture future1 = this.asyncDirectTemplate.sendAndReceive(getFooMessage()); + CompletableFuture future2 = this.asyncDirectTemplate.sendAndReceive(getFooMessage()); this.latch.get().countDown(); Message reply1 = checkMessageResult(future1, "FOO"); assertThat(reply1.getMessageProperties().getConsumerQueue()).isEqualTo(Address.AMQ_RABBITMQ_REPLY_TO); @@ -183,13 +183,13 @@ private void waitForZeroInUseConsumers() throws InterruptedException { @Test public void testMessage2Args() throws Exception { - ListenableFuture future = this.asyncTemplate.sendAndReceive(this.requests.getName(), getFooMessage()); + CompletableFuture future = this.asyncTemplate.sendAndReceive(this.requests.getName(), getFooMessage()); checkMessageResult(future, "FOO"); } @Test public void testMessage3Args() throws Exception { - ListenableFuture future = this.asyncTemplate.sendAndReceive("", this.requests.getName(), + CompletableFuture future = this.asyncTemplate.sendAndReceive("", this.requests.getName(), getFooMessage()); checkMessageResult(future, "FOO"); } @@ -197,7 +197,7 @@ public void testMessage3Args() throws Exception { @SuppressWarnings("unchecked") @Test public void testCancel() { - ListenableFuture future = this.asyncTemplate.convertSendAndReceive("foo"); + CompletableFuture future = this.asyncTemplate.convertSendAndReceive("foo"); future.cancel(false); assertThat(TestUtils.getPropertyValue(asyncTemplate, "pending", Map.class)).hasSize(0); } @@ -206,7 +206,7 @@ public void testCancel() { public void testMessageCustomCorrelation() throws Exception { Message message = getFooMessage(); message.getMessageProperties().setCorrelationId("foo"); - ListenableFuture future = this.asyncTemplate.sendAndReceive(message); + CompletableFuture future = this.asyncTemplate.sendAndReceive(message); Message result = checkMessageResult(future, "FOO"); assertThat(result.getMessageProperties().getCorrelationId()).isEqualTo("foo"); } @@ -221,7 +221,7 @@ private Message getFooMessage() { @DirtiesContext public void testReturn() throws Exception { this.asyncTemplate.setMandatory(true); - ListenableFuture future = this.asyncTemplate.convertSendAndReceive(this.requests.getName() + "x", + CompletableFuture future = this.asyncTemplate.convertSendAndReceive(this.requests.getName() + "x", "foo"); try { future.get(10, TimeUnit.SECONDS); @@ -237,7 +237,7 @@ public void testReturn() throws Exception { @DirtiesContext public void testReturnDirect() throws Exception { this.asyncDirectTemplate.setMandatory(true); - ListenableFuture future = this.asyncDirectTemplate.convertSendAndReceive(this.requests.getName() + "x", + CompletableFuture future = this.asyncDirectTemplate.convertSendAndReceive(this.requests.getName() + "x", "foo"); try { future.get(10, TimeUnit.SECONDS); @@ -254,7 +254,7 @@ public void testReturnDirect() throws Exception { public void testConvertWithConfirm() throws Exception { this.asyncTemplate.setEnableConfirms(true); RabbitConverterFuture future = this.asyncTemplate.convertSendAndReceive("sleep"); - ListenableFuture confirm = future.getConfirm(); + CompletableFuture confirm = future.getConfirm(); assertThat(confirm).isNotNull(); assertThat(confirm.get(10, TimeUnit.SECONDS)).isTrue(); checkConverterResult(future, "SLEEP"); @@ -266,7 +266,7 @@ public void testMessageWithConfirm() throws Exception { this.asyncTemplate.setEnableConfirms(true); RabbitMessageFuture future = this.asyncTemplate .sendAndReceive(new SimpleMessageConverter().toMessage("sleep", new MessageProperties())); - ListenableFuture confirm = future.getConfirm(); + CompletableFuture confirm = future.getConfirm(); assertThat(confirm).isNotNull(); assertThat(confirm.get(10, TimeUnit.SECONDS)).isTrue(); checkMessageResult(future, "SLEEP"); @@ -277,7 +277,7 @@ public void testMessageWithConfirm() throws Exception { public void testConvertWithConfirmDirect() throws Exception { this.asyncDirectTemplate.setEnableConfirms(true); RabbitConverterFuture future = this.asyncDirectTemplate.convertSendAndReceive("sleep"); - ListenableFuture confirm = future.getConfirm(); + CompletableFuture confirm = future.getConfirm(); assertThat(confirm).isNotNull(); assertThat(confirm.get(10, TimeUnit.SECONDS)).isTrue(); checkConverterResult(future, "SLEEP"); @@ -289,7 +289,7 @@ public void testMessageWithConfirmDirect() throws Exception { this.asyncDirectTemplate.setEnableConfirms(true); RabbitMessageFuture future = this.asyncDirectTemplate .sendAndReceive(new SimpleMessageConverter().toMessage("sleep", new MessageProperties())); - ListenableFuture confirm = future.getConfirm(); + CompletableFuture confirm = future.getConfirm(); assertThat(confirm).isNotNull(); assertThat(confirm.get(10, TimeUnit.SECONDS)).isTrue(); checkMessageResult(future, "SLEEP"); @@ -300,9 +300,9 @@ public void testMessageWithConfirmDirect() throws Exception { @DirtiesContext public void testReceiveTimeout() throws Exception { this.asyncTemplate.setReceiveTimeout(500); - ListenableFuture future = this.asyncTemplate.convertSendAndReceive("noReply"); + CompletableFuture future = this.asyncTemplate.convertSendAndReceive("noReply"); TheCallback callback = new TheCallback(); - future.addCallback(callback); + future.whenComplete(callback); assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class)).hasSize(1); try { future.get(10, TimeUnit.SECONDS); @@ -323,7 +323,7 @@ public void testReplyAfterReceiveTimeout() throws Exception { this.asyncTemplate.setReceiveTimeout(100); RabbitConverterFuture future = this.asyncTemplate.convertSendAndReceive("sleep"); TheCallback callback = new TheCallback(); - future.addCallback(callback); + future.whenComplete(callback); assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class)).hasSize(1); try { future.get(10, TimeUnit.SECONDS); @@ -342,7 +342,7 @@ public void testReplyAfterReceiveTimeout() throws Exception { * map when it times out. However, there is a small race condition where * the reply arrives at the same time as the timeout. */ - future.set("foo"); + future.complete("foo"); assertThat(callback.result).isNull(); } @@ -353,7 +353,7 @@ public void testStopCancelled() throws Exception { this.asyncTemplate.setReceiveTimeout(5000); RabbitConverterFuture future = this.asyncTemplate.convertSendAndReceive("noReply"); TheCallback callback = new TheCallback(); - future.addCallback(callback); + future.whenComplete(callback); assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class)).hasSize(1); this.asyncTemplate.stop(); // Second stop() to be sure that it is idempotent @@ -375,54 +375,76 @@ public void testStopCancelled() throws Exception { * should never happen because the container is stopped before canceling * and the future is removed from the pending map. */ - future.set("foo"); + future.complete("foo"); assertThat(callback.result).isNull(); } - private void checkConverterResult(ListenableFuture future, String expected) throws InterruptedException { + @Test + void ctorCoverage() { + AsyncRabbitTemplate template = new AsyncRabbitTemplate(mock(ConnectionFactory.class), "ex", "rk"); + assertThat(template).extracting(t -> t.getRabbitTemplate()) + .extracting("exchange") + .isEqualTo("ex"); + assertThat(template).extracting(t -> t.getRabbitTemplate()) + .extracting("routingKey") + .isEqualTo("rk"); + template = new AsyncRabbitTemplate(mock(ConnectionFactory.class), "ex", "rk", "rq"); + assertThat(template).extracting(t -> t.getRabbitTemplate()) + .extracting("exchange") + .isEqualTo("ex"); + assertThat(template).extracting(t -> t.getRabbitTemplate()) + .extracting("routingKey") + .isEqualTo("rk"); + assertThat(template) + .extracting("replyAddress") + .isEqualTo("rq"); + assertThat(template).extracting("container") + .extracting("queueNames") + .isEqualTo(new String[] { "rq" }); + template = new AsyncRabbitTemplate(mock(ConnectionFactory.class), "ex", "rk", "rq", "ra"); + assertThat(template).extracting(t -> t.getRabbitTemplate()) + .extracting("exchange") + .isEqualTo("ex"); + assertThat(template).extracting(t -> t.getRabbitTemplate()) + .extracting("routingKey") + .isEqualTo("rk"); + assertThat(template) + .extracting("replyAddress") + .isEqualTo("ra"); + assertThat(template).extracting("container") + .extracting("queueNames") + .isEqualTo(new String[] { "rq" }); + template = new AsyncRabbitTemplate(mock(RabbitTemplate.class), mock(AbstractMessageListenerContainer.class), + "rq"); + assertThat(template) + .extracting("replyAddress") + .isEqualTo("rq"); + } + + private void checkConverterResult(CompletableFuture future, String expected) throws InterruptedException { final CountDownLatch cdl = new CountDownLatch(1); final AtomicReference resultRef = new AtomicReference<>(); - future.addCallback(new ListenableFutureCallback() { - - @Override - public void onSuccess(String result) { - resultRef.set(result); - cdl.countDown(); - } - - @Override - public void onFailure(Throwable ex) { - cdl.countDown(); - } - + future.whenComplete((result, ex) -> { + resultRef.set(result); + cdl.countDown(); }); assertThat(cdl.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(resultRef.get()).isEqualTo(expected); } - private Message checkMessageResult(ListenableFuture future, String expected) throws InterruptedException { + private Message checkMessageResult(CompletableFuture future, String expected) throws InterruptedException { final CountDownLatch cdl = new CountDownLatch(1); final AtomicReference resultRef = new AtomicReference<>(); - future.addCallback(new ListenableFutureCallback() { - - @Override - public void onSuccess(Message result) { - resultRef.set(result); - cdl.countDown(); - } - - @Override - public void onFailure(Throwable ex) { - cdl.countDown(); - } - + future.whenComplete((result, ex) -> { + resultRef.set(result); + cdl.countDown(); }); assertThat(cdl.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(new String(resultRef.get().getBody())).isEqualTo(expected); return resultRef.get(); } - public static class TheCallback implements ListenableFutureCallback { + public static class TheCallback implements BiConsumer { private final CountDownLatch latch = new CountDownLatch(1); @@ -430,14 +452,10 @@ public static class TheCallback implements ListenableFutureCallback { private volatile Throwable ex; - @Override - public void onSuccess(String result) { - this.result = result; - latch.countDown(); - } @Override - public void onFailure(Throwable ex) { + public void accept(String result, Throwable ex) { + this.result = result; this.ex = ex; latch.countDown(); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AsyncListenerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AsyncListenerTests.java index d5ce86409a..459cdb070e 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AsyncListenerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AsyncListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-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. @@ -24,6 +24,7 @@ import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -36,7 +37,7 @@ import org.springframework.amqp.core.AnonymousQueue; import org.springframework.amqp.core.Queue; import org.springframework.amqp.rabbit.AsyncRabbitTemplate; -import org.springframework.amqp.rabbit.AsyncRabbitTemplate.RabbitConverterFuture; +import org.springframework.amqp.rabbit.RabbitConverterFuture; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; @@ -53,8 +54,6 @@ import org.springframework.stereotype.Component; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.SettableListenableFuture; import reactor.core.publisher.Mono; @@ -284,13 +283,13 @@ public static class Listener { private final AtomicBoolean first7 = new AtomicBoolean(true); @RabbitListener(id = "foo", queues = "#{queue1.name}") - public ListenableFuture listen1(String foo) { - SettableListenableFuture future = new SettableListenableFuture<>(); + public CompletableFuture listen1(String foo) { + CompletableFuture future = new CompletableFuture<>(); if (fooFirst.getAndSet(false)) { - future.setException(new RuntimeException("Future.exception")); + future.completeExceptionally(new RuntimeException("Future.exception")); } else { - future.set(foo.toUpperCase()); + future.complete(foo.toUpperCase()); } return future; } @@ -311,17 +310,17 @@ public Mono> listen3(String foo) { } @RabbitListener(id = "qux", queues = "#{queue4.name}") - public ListenableFuture listen4(@SuppressWarnings("unused") String foo) { - SettableListenableFuture future = new SettableListenableFuture<>(); - future.set(null); + public CompletableFuture listen4(@SuppressWarnings("unused") String foo) { + CompletableFuture future = new CompletableFuture<>(); + future.complete(null); this.latch4.countDown(); return future; } @RabbitListener(id = "fiz", queues = "#{queue5.name}") - public ListenableFuture listen5(@SuppressWarnings("unused") String foo) { - SettableListenableFuture future = new SettableListenableFuture<>(); - future.setException(new AmqpRejectAndDontRequeueException("asyncToDLQ")); + public CompletableFuture listen5(@SuppressWarnings("unused") String foo) { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(new AmqpRejectAndDontRequeueException("asyncToDLQ")); return future; } @@ -331,9 +330,9 @@ public void listen5DLQ(@SuppressWarnings("unused") String foo) { } @RabbitListener(id = "fix", queues = "#{queue6.name}", containerFactory = "dontRequeueFactory") - public ListenableFuture listen6(@SuppressWarnings("unused") String foo) { - SettableListenableFuture future = new SettableListenableFuture<>(); - future.setException(new IllegalStateException("asyncDefaultToDLQ")); + public CompletableFuture listen6(@SuppressWarnings("unused") String foo) { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(new IllegalStateException("asyncDefaultToDLQ")); return future; } @@ -344,13 +343,13 @@ public void listen6DLQ(@SuppressWarnings("unused") String foo) { @RabbitListener(id = "overrideFactoryRequeue", queues = "#{queue7.name}", containerFactory = "dontRequeueFactory") - public ListenableFuture listen7(@SuppressWarnings("unused") String foo) { - SettableListenableFuture future = new SettableListenableFuture<>(); + public CompletableFuture listen7(@SuppressWarnings("unused") String foo) { + CompletableFuture future = new CompletableFuture<>(); if (this.first7.compareAndSet(true, false)) { - future.setException(new ImmediateRequeueAmqpException("asyncOverrideDefaultToDLQ")); + future.completeExceptionally(new ImmediateRequeueAmqpException("asyncOverrideDefaultToDLQ")); } else { - future.set("listen7"); + future.complete("listen7"); } return future; } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ComplexTypeJsonIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ComplexTypeJsonIntegrationTests.java index 1c2a504f3a..2b9d34276f 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ComplexTypeJsonIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ComplexTypeJsonIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-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. @@ -24,7 +24,7 @@ import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.AsyncRabbitTemplate; -import org.springframework.amqp.rabbit.AsyncRabbitTemplate.RabbitConverterFuture; +import org.springframework.amqp.rabbit.RabbitConverterFuture; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/MessagingTemplateConfirmsTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/MessagingTemplateConfirmsTests.java index e48dc3b206..51a70bb93d 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/MessagingTemplateConfirmsTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/MessagingTemplateConfirmsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-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. diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests2.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests2.java index 8d46fb248f..450c1566a8 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests2.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests2.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 the original author or authors. + * Copyright 2016-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. diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/AsyncReplyToTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/AsyncReplyToTests.java index 3bfab60541..e5c43a5fd7 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/AsyncReplyToTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/AsyncReplyToTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-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. @@ -19,6 +19,7 @@ import static org.assertj.core.api.Assertions.assertThat; import java.io.IOException; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -44,8 +45,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.SettableListenableFuture; import com.rabbitmq.client.Channel; @@ -97,7 +96,7 @@ void ackSingleWhenFatalDMLC(@Autowired Config config, @Autowired RabbitListenerE .build()); assertThat(config.dmlcLatch.await(10, TimeUnit.SECONDS)).isTrue(); registry.getListenerContainer("dmlc").stop(); - assertThat(admin.getQueueInfo("async2").getMessageCount()).isEqualTo(1); + assertThat(admin.getQueueInfo("async2").getMessageCount()).isEqualTo(0); } @Configuration @@ -109,13 +108,15 @@ static class Config { volatile CountDownLatch dmlcLatch = new CountDownLatch(1); @RabbitListener(id = "smlc", queues = "async1", containerFactory = "smlcf") - ListenableFuture listen1(String in, Channel channel) { - return new SettableListenableFuture<>(); + CompletableFuture listen1(String in, Channel channel) { + return new CompletableFuture<>(); } @RabbitListener(id = "dmlc", queues = "async2", containerFactory = "dmlcf") - ListenableFuture listen2(String in, Channel channel) { - return new SettableListenableFuture<>(); + CompletableFuture listen2(String in, Channel channel) { + CompletableFuture future = new CompletableFuture<>(); + future.complete("test"); + return future; } @Bean diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapterTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapterTests.java index 15b6501988..0db0b7aaae 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapterTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -27,6 +27,7 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -43,8 +44,6 @@ import org.springframework.retry.RetryPolicy; import org.springframework.retry.policy.SimpleRetryPolicy; import org.springframework.retry.support.RetryTemplate; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.SettableListenableFuture; import com.rabbitmq.client.Channel; import reactor.core.publisher.Mono; @@ -220,13 +219,13 @@ public void testReplyRetry() throws Exception { } @Test - public void testListenableFutureReturn() throws Exception { + public void testCompletableFutureReturn() throws Exception { class Delegate { @SuppressWarnings("unused") - public ListenableFuture myPojoMessageMethod(String input) { - SettableListenableFuture future = new SettableListenableFuture<>(); - future.set("processed" + input); + public CompletableFuture myPojoMessageMethod(String input) { + CompletableFuture future = new CompletableFuture<>(); + future.complete("processed" + input); return future; } diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index a0931518fb..fae9a422f3 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -1224,7 +1224,7 @@ This is no longer necessary since the framework now hands off the callback invoc IMPORTANT: The guarantee of receiving a returned message before the ack is still maintained as long as the return callback executes in 60 seconds or less. The confirm is scheduled to be delivered after the return callback exits or after 60 seconds, whichever comes first. -Starting with version 2.1, the `CorrelationData` object has a `ListenableFuture` that you can use to get the result, instead of using a `ConfirmCallback` on the template. +The `CorrelationData` object has a `CompletableFuture` that you can use to get the result, instead of using a `ConfirmCallback` on the template. The following example shows how to configure a `CorrelationData` instance: ==== @@ -1236,7 +1236,7 @@ assertTrue(cd1.getFuture().get(10, TimeUnit.SECONDS).isAck()); ---- ==== -Since it is a `ListenableFuture`, you can either `get()` the result when ready or add listeners for an asynchronous callback. +Since it is a `CompletableFuture`, you can either `get()` the result when ready or use `whenComplete()` for an asynchronous callback. The `Confirm` object is a simple bean with 2 properties: `ack` and `reason` (for `nack` instances). The reason is not populated for broker-generated `nack` instances. It is populated for `nack` instances generated by the framework (for example, closing the connection while `ack` instances are outstanding). @@ -3569,7 +3569,8 @@ IMPORTANT: Containers created this way are normal `@Bean` instances and are not [[async-returns]] ===== Asynchronous `@RabbitListener` Return Types -Starting with version 2.1, `@RabbitListener` (and `@RabbitHandler`) methods can be specified with asynchronous return types `ListenableFuture` and `Mono`, letting the reply be sent asynchronously. +`@RabbitListener` (and `@RabbitHandler`) methods can be specified with asynchronous return types `CompletableFuture` and `Mono`, letting the reply be sent asynchronously. +`ListenableFuture` is no longer supported; it has been deprecated by Spring Framework. IMPORTANT: 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. @@ -4555,7 +4556,7 @@ You can also take a look at the `FixedReplyQueueDeadLetterTests` test case for a Version 1.6 introduced the `AsyncRabbitTemplate`. This has similar `sendAndReceive` (and `convertSendAndReceive`) methods to those on the <>. -However, instead of blocking, they return a `ListenableFuture`. +However, instead of blocking, they return a `CompletableFuture`. The `sendAndReceive` methods return a `RabbitMessageFuture`. The `convertSendAndReceive` methods return a `RabbitConverterFuture`. @@ -4575,13 +4576,13 @@ public void doSomeWorkAndGetResultLater() { ... - ListenableFuture future = this.template.convertSendAndReceive("foo"); + CompletableFuture future = this.template.convertSendAndReceive("foo"); // do some more work String reply = null; try { - reply = future.get(); + reply = future.get(10, TimeUnit.SECONDS); } catch (ExecutionException e) { ... @@ -4596,18 +4597,13 @@ public void doSomeWorkAndGetResultAsync() { ... RabbitConverterFuture future = this.template.convertSendAndReceive("foo"); - future.addCallback(new ListenableFutureCallback() { - - @Override - public void onSuccess(String result) { - ... + future.whenComplete((result, ex) -> { + if (ex == null) { + // success } - - @Override - public void onFailure(Throwable ex) { - ... + else { + // failure } - }); ... @@ -4618,7 +4614,7 @@ public void doSomeWorkAndGetResultAsync() { If `mandatory` is set and the message cannot be delivered, the future throws an `ExecutionException` with a cause of `AmqpMessageReturnedException`, which encapsulates the returned message and information about the return. -If `enableConfirms` is set, the future has a property called `confirm`, which is itself a `ListenableFuture` with `true` indicating a successful publish. +If `enableConfirms` is set, the future has a property called `confirm`, which is itself a `CompletableFuture` with `true` indicating a successful publish. If the confirm future is `false`, the `RabbitFuture` has a further property called `nackCause`, which contains the reason for the failure, if available. IMPORTANT: The publisher confirm is discarded if it is received after the reply, since the reply implies a successful publish. @@ -4647,6 +4643,8 @@ Version 2.0 introduced variants of these methods (`convertSendAndReceiveAsType`) You must configure the underlying `RabbitTemplate` with a `SmartMessageConverter`. See <> for more information. +The `AsyncRabbitTemplate2` (added to assist with migration to this release) is now deprecated in favor of `AsyncRabbitTemplate` which now returns `CompletableFuture` s instead of `ListenableFuture` s. + [[remoting]] ===== Spring Remoting with AMQP diff --git a/src/reference/asciidoc/appendix.adoc b/src/reference/asciidoc/appendix.adoc index d1f3bf6d2b..b7e6ce4282 100644 --- a/src/reference/asciidoc/appendix.adoc +++ b/src/reference/asciidoc/appendix.adoc @@ -34,6 +34,16 @@ Support remoting using Spring Framework’s RMI support is deprecated and will b The `Jackson2JsonMessageConverter` can now determine the charset from the `contentEncoding` header. See <> for more information. +==== AsyncRabbitTemplate + +The `AsyncRabbitTemplate` is deprecated in favor of `AsyncRabbitTemplate2` which returns `CompletableFuture` s instead of `ListenableFuture` s. +See <> for more information. + +==== Message Converter Changes + +The `Jackson2JsonMessageConverter` can now determine the charset from the `contentEncoding` header. +See <> for more information. + ==== Changes in 2.3 Since 2.2 This section describes the changes between version 2.2 and version 2.3. diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index fb15a9e091..f355f76990 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -10,3 +10,9 @@ This version requires Spring Framework 6.0 and Java 17 ==== Remoting The remoting feature (using RMI) is no longer supported. + +==== AsyncRabbitTemplate + +The `AsyncRabbitTemplate2`, which was added in 2.4.7 to aid migration to this release, is deprecated in favor of `AsyncRabbitTemplate`. +The `AsyncRabbitTemplate` now returns `CompletableFuture` s instead of `ListenableFuture` s. +See <> for more information. From 97a508e37917559d9c37ca8961b33330e3f055f3 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 28 Jul 2022 16:06:14 -0400 Subject: [PATCH 127/737] GH-1480: Switch to CompletableFuture in s-r-stream Resolves https://github.com/spring-projects/spring-amqp/issues/1480 Also reinstate deprecated `AsyncAmqpTemplate2`. --- .../amqp/core/AsyncAmqpTemplate2.java | 32 ++++++++++++++++ .../producer/RabbitStreamOperations.java | 11 +++--- .../producer/RabbitStreamOperations2.java | 34 +++++++++++++++++ .../stream/producer/RabbitStreamTemplate.java | 28 +++++++------- .../producer/RabbitStreamTemplate2.java | 38 +++++++++++++++++++ .../amqp/rabbit/AsyncRabbitTemplate2.java | 3 +- src/reference/asciidoc/appendix.adoc | 5 +++ src/reference/asciidoc/stream.adoc | 2 + src/reference/asciidoc/whats-new.adoc | 5 +++ 9 files changed, 138 insertions(+), 20 deletions(-) create mode 100644 spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate2.java create mode 100644 spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations2.java create mode 100644 spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate2.java diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate2.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate2.java new file mode 100644 index 0000000000..cf835e7884 --- /dev/null +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate2.java @@ -0,0 +1,32 @@ +/* + * 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.core; + +import java.util.concurrent.CompletableFuture; + +/** + * This interface was added in 2.4.7 to aid migration from methods returning + * {@code ListenableFuture}s to {@link CompletableFuture}s. + * + * @author Gary Russell + * @since 2.4.7 + * @deprecated in favor of {@link AsyncAmqpTemplate}. + */ +@Deprecated +public interface AsyncAmqpTemplate2 extends AsyncAmqpTemplate { + +} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations.java index bf3a8ea683..bdd2eae26d 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations.java @@ -16,13 +16,14 @@ package org.springframework.rabbit.stream.producer; +import java.util.concurrent.CompletableFuture; + import org.springframework.amqp.AmqpException; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessagePostProcessor; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.lang.Nullable; import org.springframework.rabbit.stream.support.converter.StreamMessageConverter; -import org.springframework.util.concurrent.ListenableFuture; import com.rabbitmq.stream.MessageBuilder; @@ -40,14 +41,14 @@ public interface RabbitStreamOperations extends AutoCloseable { * @param message the message. * @return a future to indicate success/failure. */ - ListenableFuture send(Message message); + CompletableFuture send(Message message); /** * Convert to and send a Spring AMQP message. * @param message the payload. * @return a future to indicate success/failure. */ - ListenableFuture convertAndSend(Object message); + CompletableFuture convertAndSend(Object message); /** * Convert to and send a Spring AMQP message. If a {@link MessagePostProcessor} is @@ -57,7 +58,7 @@ public interface RabbitStreamOperations extends AutoCloseable { * @param mpp a message post processor. * @return a future to indicate success/failure. */ - ListenableFuture convertAndSend(Object message, @Nullable MessagePostProcessor mpp); + CompletableFuture convertAndSend(Object message, @Nullable MessagePostProcessor mpp); /** * Send a native stream message. @@ -65,7 +66,7 @@ public interface RabbitStreamOperations extends AutoCloseable { * @return a future to indicate success/failure. * @see #messageBuilder() */ - ListenableFuture send(com.rabbitmq.stream.Message message); + CompletableFuture send(com.rabbitmq.stream.Message message); /** * Return the producer's {@link MessageBuilder} to create native stream messages. diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations2.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations2.java new file mode 100644 index 0000000000..e1e9323748 --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations2.java @@ -0,0 +1,34 @@ +/* + * 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.rabbit.stream.producer; + +import java.util.concurrent.CompletableFuture; + +/** + * Provides methods for sending messages using a RabbitMQ Stream producer, + * returning {@link CompletableFuture}. + * This interface was added in 2.4.7 to aid migration from methods returning + * {@code ListenableFuture}s to {@link CompletableFuture}s. + * + * @author Gary Russell + * @since 2.4.7 + * @deprecated in favor of {@link RabbitStreamOperations}. + */ +@Deprecated +public interface RabbitStreamOperations2 extends RabbitStreamOperations { + +} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java index e8c14bc6c6..e8483935fb 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-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. @@ -16,6 +16,8 @@ package org.springframework.rabbit.stream.producer; +import java.util.concurrent.CompletableFuture; + import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessagePostProcessor; import org.springframework.amqp.support.converter.MessageConverter; @@ -27,8 +29,6 @@ import org.springframework.rabbit.stream.support.converter.DefaultStreamMessageConverter; import org.springframework.rabbit.stream.support.converter.StreamMessageConverter; import org.springframework.util.Assert; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.SettableListenableFuture; import com.rabbitmq.stream.ConfirmationHandler; import com.rabbitmq.stream.Constants; @@ -138,27 +138,27 @@ public StreamMessageConverter streamMessageConverter() { @Override - public ListenableFuture send(Message message) { - SettableListenableFuture future = new SettableListenableFuture<>(); + public CompletableFuture send(Message message) { + CompletableFuture future = new CompletableFuture<>(); createOrGetProducer().send(this.streamConverter.fromMessage(message), handleConfirm(future)); return future; } @Override - public ListenableFuture convertAndSend(Object message) { + public CompletableFuture convertAndSend(Object message) { return convertAndSend(message, null); } @Override - public ListenableFuture convertAndSend(Object message, @Nullable MessagePostProcessor mpp) { + public CompletableFuture convertAndSend(Object message, @Nullable MessagePostProcessor mpp) { Message message2 = this.messageConverter.toMessage(message, new StreamMessageProperties()); Assert.notNull(message2, "The message converter returned null"); if (mpp != null) { message2 = mpp.postProcessMessage(message2); if (message2 == null) { this.logger.debug("Message Post Processor returned null, message not sent"); - SettableListenableFuture future = new SettableListenableFuture<>(); - future.set(false); + CompletableFuture future = new CompletableFuture<>(); + future.complete(false); return future; } } @@ -167,8 +167,8 @@ public ListenableFuture convertAndSend(Object message, @Nullable Messag @Override - public ListenableFuture send(com.rabbitmq.stream.Message message) { - SettableListenableFuture future = new SettableListenableFuture<>(); + public CompletableFuture send(com.rabbitmq.stream.Message message) { + CompletableFuture future = new CompletableFuture<>(); createOrGetProducer().send(message, handleConfirm(future)); return future; } @@ -178,10 +178,10 @@ public MessageBuilder messageBuilder() { return createOrGetProducer().messageBuilder(); } - private ConfirmationHandler handleConfirm(SettableListenableFuture future) { + private ConfirmationHandler handleConfirm(CompletableFuture future) { return confStatus -> { if (confStatus.isConfirmed()) { - future.set(true); + future.complete(true); } else { int code = confStatus.getCode(); @@ -203,7 +203,7 @@ private ConfirmationHandler handleConfirm(SettableListenableFuture futu errorMessage = "Unknown code: " + code; break; } - future.setException(new StreamSendException(errorMessage, code)); + future.completeExceptionally(new StreamSendException(errorMessage, code)); } }; } diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate2.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate2.java new file mode 100644 index 0000000000..664515f404 --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate2.java @@ -0,0 +1,38 @@ +/* + * 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.rabbit.stream.producer; + +import java.util.concurrent.CompletableFuture; + +import com.rabbitmq.stream.Environment; + +/** + * This interface was added in 2.4.7 to aid migration from methods returning + * {@code ListenableFuture}s to {@link CompletableFuture}s. + * + * @author Gary Russell + * @since 2.8 + * @deprecated in favor of {@link RabbitStreamTemplate}. + */ +@Deprecated +public class RabbitStreamTemplate2 extends RabbitStreamTemplate implements RabbitStreamOperations2 { + + public RabbitStreamTemplate2(Environment environment, String streamName) { + super(environment, streamName); + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate2.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate2.java index a02af02c99..0bc828ec60 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate2.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate2.java @@ -18,6 +18,7 @@ import java.util.concurrent.CompletableFuture; +import org.springframework.amqp.core.AsyncAmqpTemplate2; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; @@ -32,7 +33,7 @@ * */ @Deprecated -public class AsyncRabbitTemplate2 extends AsyncRabbitTemplate { +public class AsyncRabbitTemplate2 extends AsyncRabbitTemplate implements AsyncAmqpTemplate2 { public AsyncRabbitTemplate2(ConnectionFactory connectionFactory, String exchange, String routingKey, String replyQueue, String replyAddress) { diff --git a/src/reference/asciidoc/appendix.adoc b/src/reference/asciidoc/appendix.adoc index b7e6ce4282..a48fb8d433 100644 --- a/src/reference/asciidoc/appendix.adoc +++ b/src/reference/asciidoc/appendix.adoc @@ -44,6 +44,11 @@ See <> for more information. The `Jackson2JsonMessageConverter` can now determine the charset from the `contentEncoding` header. See <> for more information. +==== Stream Support Changes + +`RabbitStreamOperations` and `RabbitStreamTemplate` have been deprecated in favor of `RabbitStreamOperations2` and `RabbitStreamTemplate2` respectively; they return `CompletableFuture` instead of `ListenableFuture`. +See <> for more information. + ==== Changes in 2.3 Since 2.2 This section describes the changes between version 2.2 and version 2.3. diff --git a/src/reference/asciidoc/stream.adoc b/src/reference/asciidoc/stream.adoc index a77374330b..bc18e09164 100644 --- a/src/reference/asciidoc/stream.adoc +++ b/src/reference/asciidoc/stream.adoc @@ -67,6 +67,8 @@ The `ProducerCustomizer` provides a mechanism to customize the producer before i Refer to the https://rabbitmq.github.io/rabbitmq-stream-java-client/stable/htmlsingle/[Java Client Documentation] about customizing the `Environment` and `Producer`. +IMPORTANT: In version 2.4.7 `RabbitStreamOperations2` and `RabbitStreamTemplate2` were added to assist migration to this version; `RabbitStreamOperations2` and `RabbitStreamTemplate2` are now deprecated in favor of `RabbitStreamOperations` and `RabbitStreamTemplate` respectively. + ==== Receiving Messages Asynchronous message reception is provided by the `StreamListenerContainer` (and the `StreamRabbitListenerContainerFactory` when using `@RabbitListener`). diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index f355f76990..07cfb6f15f 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -16,3 +16,8 @@ The remoting feature (using RMI) is no longer supported. The `AsyncRabbitTemplate2`, which was added in 2.4.7 to aid migration to this release, is deprecated in favor of `AsyncRabbitTemplate`. The `AsyncRabbitTemplate` now returns `CompletableFuture` s instead of `ListenableFuture` s. See <> for more information. + +==== Stream Support Changes + +`RabbitStreamOperations2` and `RabbitStreamTemplate2` have been deprecated in favor of `RabbitStreamOperations` and `RabbitStreamTemplate` respectively. +See <> for more information. From 515eb9a32b8d1e57e52f335ea636f88ccd344297 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 2 Aug 2022 17:46:22 -0400 Subject: [PATCH 128/737] GH-1484: RabbitListener.batch() Override Resolves https://github.com/spring-projects/spring-amqp/issues/1484 --- .../amqp/rabbit/annotation/RabbitListener.java | 14 ++++++++++++++ ...abbitListenerAnnotationBeanPostProcessor.java | 3 +++ .../AbstractRabbitListenerContainerFactory.java | 4 +++- .../listener/AbstractRabbitListenerEndpoint.java | 16 +++++++++++++++- .../listener/MethodRabbitListenerEndpoint.java | 11 ++++++----- .../rabbit/listener/RabbitListenerEndpoint.java | 8 ++++++++ .../EnableRabbitBatchIntegrationTests.java | 6 +++--- 7 files changed, 52 insertions(+), 10 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListener.java index 51c43847c4..8b3787075e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListener.java @@ -332,4 +332,18 @@ */ String converterWinsContentType() default "true"; + /** + * Override the container factory's {@code batchListener} property. The listener + * method signature should receive a {@code List}; refer to the reference + * documentation. This allows a single container factory to be used for both record + * and batch listeners; previously separate container factories were required. + * @return "true" for the annotated method to be a batch listener or "false" for a + * single message listener. If not set, the container factory setting is used. SpEL and + * property place holders are not supported because the listener type cannot be + * variable. + * @since 3.0 + * @see Boolean#parseBoolean(String) + */ + String batch() default ""; + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java index 74983a7320..e72f0a85fb 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java @@ -461,6 +461,9 @@ protected Collection processListener(MethodRabbitListenerEndpoint en resolvePostProcessor(endpoint, rabbitListener, target, beanName); resolveMessageConverter(endpoint, rabbitListener, target, beanName); resolveReplyContentType(endpoint, rabbitListener); + if (StringUtils.hasText(rabbitListener.batch())) { + endpoint.setBatchListener(Boolean.parseBoolean(rabbitListener.batch())); + } RabbitListenerContainerFactory factory = resolveContainerFactory(rabbitListener, target, beanName); this.registrar.registerEndpoint(endpoint, factory); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java index bb1f6b1ffb..086d8ff1dc 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java @@ -381,7 +381,9 @@ public C createListenerContainer(RabbitListenerEndpoint endpoint) { .acceptIfNotNull(endpoint.getAckMode(), instance::setAcknowledgeMode) .acceptIfNotNull(endpoint.getBatchingStrategy(), instance::setBatchingStrategy); instance.setListenerId(endpoint.getId()); - endpoint.setBatchListener(this.batchListener); + if (endpoint.getBatchListener() == null) { + endpoint.setBatchListener(this.batchListener); + } } applyCommonOverrides(endpoint, instance); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java index 6aaaf7c700..8cac1fc019 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java @@ -84,7 +84,7 @@ public abstract class AbstractRabbitListenerEndpoint implements RabbitListenerEn private TaskExecutor taskExecutor; - private boolean batchListener; + private Boolean batchListener; private BatchingStrategy batchingStrategy; @@ -293,7 +293,21 @@ public void setTaskExecutor(TaskExecutor taskExecutor) { this.taskExecutor = taskExecutor; } + /** + * True if this endpoint is for a batch listener. + * @return true if batch. + */ public boolean isBatchListener() { + return this.batchListener == null ? false : this.batchListener; + } + + /** + * True if this endpoint is for a batch listener. + * @return {@link Boolean#TRUE} if batch. + * @since 3.0 + */ + @Nullable + public Boolean getBatchListener() { return this.batchListener; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MethodRabbitListenerEndpoint.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MethodRabbitListenerEndpoint.java index baea9e49ae..171b1005cd 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MethodRabbitListenerEndpoint.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MethodRabbitListenerEndpoint.java @@ -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. @@ -130,7 +130,7 @@ public void setAdapterProvider(AdapterProvider adapterProvider) { protected MessagingMessageListenerAdapter createMessageListener(MessageListenerContainer container) { Assert.state(this.messageHandlerMethodFactory != null, "Could not create message listener - MessageHandlerMethodFactory not set"); - MessagingMessageListenerAdapter messageListener = createMessageListenerInstance(); + MessagingMessageListenerAdapter messageListener = createMessageListenerInstance(getBatchListener()); messageListener.setHandlerAdapter(configureListenerAdapter(messageListener)); String replyToAddress = getDefaultReplyToAddress(); if (replyToAddress != null) { @@ -159,11 +159,12 @@ protected HandlerAdapter configureListenerAdapter(MessagingMessageListenerAdapte /** * Create an empty {@link MessagingMessageListenerAdapter} instance. + * @param batch whether this endpoint is for a batch listener. * @return the {@link MessagingMessageListenerAdapter} instance. */ - protected MessagingMessageListenerAdapter createMessageListenerInstance() { - return this.adapterProvider.getAdapter(isBatchListener(), this.bean, this.method, this.returnExceptions, - this.errorHandler, getBatchingStrategy()); + protected MessagingMessageListenerAdapter createMessageListenerInstance(@Nullable Boolean batch) { + return this.adapterProvider.getAdapter(batch == null ? isBatchListener() : batch, this.bean, this.method, + this.returnExceptions, this.errorHandler, getBatchingStrategy()); } @Nullable diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpoint.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpoint.java index 787a9473d5..062f40a524 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpoint.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpoint.java @@ -116,6 +116,14 @@ default TaskExecutor getTaskExecutor() { default void setBatchListener(boolean batchListener) { } + /** + * Whether this endpoint is for a batch listener. + * @return {@link Boolean#TRUE} if batch. + * @since 3.0 + */ + @Nullable + Boolean getBatchListener(); + /** * Set a {@link BatchingStrategy} to use when debatching messages. * @param batchingStrategy the batching strategy. diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchIntegrationTests.java index 2c37e3b203..6837be5121 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2020 the original author or authors. + * Copyright 2019-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. @@ -143,7 +143,7 @@ public DirectRabbitListenerContainerFactory directListenerContainerFactory() { public SimpleRabbitListenerContainerFactory consumerBatchContainerFactory() { SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); factory.setConnectionFactory(connectionFactory()); - factory.setBatchListener(true); + factory.setBatchListener(false); factory.setConsumerBatchEnabled(true); factory.setBatchSize(2); return factory; @@ -202,7 +202,7 @@ public void listen2(List> in) { this.fooMessagesLatch.countDown(); } - @RabbitListener(queues = "batch.3", containerFactory = "consumerBatchContainerFactory") + @RabbitListener(queues = "batch.3", containerFactory = "consumerBatchContainerFactory", batch = "true") public void listen3(List in) { this.foosConsumerBatchToo = in; this.fooConsumerBatchTooLatch.countDown(); From d96aa71ec170ee2665d351d80bba717adb41db42 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 4 Aug 2022 15:41:45 -0400 Subject: [PATCH 129/737] 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 --- .../SimpleRabbitListenerContainerFactory.java | 9 ++- .../MessagingMessageListenerAdapter.java | 15 ++++- .../EnableRabbitBatchIntegrationTests.java | 24 +++++++- ...hMessagingMessageListenerAdapterTests.java | 55 +++++++++++++++++++ src/reference/asciidoc/amqp.adoc | 17 +++--- src/reference/asciidoc/whats-new.adoc | 7 +++ 6 files changed, 115 insertions(+), 12 deletions(-) create mode 100644 spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapterTests.java diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerContainerFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerContainerFactory.java index a82840e62c..2e8c5afdd6 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerContainerFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerContainerFactory.java @@ -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 void setReceiveTimeout(Long receiveTimeout) { /** * 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 diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java index a409ec4232..41c3ebdcd8 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java @@ -20,6 +20,7 @@ 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 @@ protected final class MessagingMessageConverterAdapter extends MessagingMessageC private boolean isAmqpMessageList; + private boolean isCollection; + MessagingMessageConverterAdapter(Object bean, Method method, boolean batch) { this.bean = bean; this.method = method; @@ -392,6 +395,12 @@ private Type determineInferredType() { // NOSONAR - complexity 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 @@ private Type extractGenericParameterTypFromMethodParameter(MethodParameter metho 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); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchIntegrationTests.java index 6837be5121..4bcb6f92bc 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchIntegrationTests.java @@ -19,6 +19,8 @@ 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 @@ */ @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 void nativeMessageList() throws InterruptedException { .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 static class Listener { CountDownLatch fooConsumerBatchTooLatch = new CountDownLatch(1); + List fives = new ArrayList<>(); + + CountDownLatch fivesLatch = new CountDownLatch(1); + private List nativeMessages; private final CountDownLatch nativeMessagesLatch = new CountDownLatch(1); @@ -214,6 +230,12 @@ public void listen4(List in) { this.nativeMessagesLatch.countDown(); } + @RabbitListener(queues = "batch.5") + public void listen5(Collection in) { + this.fives.addAll(in); + this.fivesLatch.countDown(); + } + } @SuppressWarnings("serial") diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapterTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapterTests.java new file mode 100644 index 0000000000..358c4676b4 --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapterTests.java @@ -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 in) { + } + +} diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index fae9a422f3..8cf2ae89c7 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -3422,7 +3422,7 @@ Adding a `group` attribute causes a bean of type `Collection> 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 amqpMessages) { - this.amqpMessagesReceived = amqpMessages; - this.batch1Latch.countDown(); + ... } @RabbitListener(queues = "batch.2", containerFactory = "consumerBatchContainerFactory") public void consumerBatch2(List> messages) { - this.messagingMessagesReceived = messages; - this.batch2Latch.countDown(); + ... } @RabbitListener(queues = "batch.3", containerFactory = "consumerBatchContainerFactory") public void consumerBatch3(List strings) { - this.batch3Strings = strings; - this.batch3Latch.countDown(); + ... } ---- ==== @@ -3511,6 +3508,12 @@ public void consumerBatch3(List 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 diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index 07cfb6f15f..b32794367d 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -21,3 +21,10 @@ See <> for more information. `RabbitStreamOperations2` and `RabbitStreamTemplate2` have been deprecated in favor of `RabbitStreamOperations` and `RabbitStreamTemplate` respectively. See <> 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 <> for more infoprmation. From 158d5cd5e2200036329f675ae70297a80e1fd3bb Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 9 Aug 2022 15:44:58 -0400 Subject: [PATCH 130/737] GH-1491: Support Optional/null Payloads Resolves https://github.com/spring-projects/spring-amqp/issues/1491 **cherry-pick to 2.4.x** * Improve connection factory bean in test. --- .../AbstractJackson2MessageConverter.java | 19 ++- ...itListenerAnnotationBeanPostProcessor.java | 72 ++++++++- .../MessagingMessageListenerAdapter.java | 9 ++ .../annotation/OptionalPayloadTests.java | 142 ++++++++++++++++++ src/reference/asciidoc/amqp.adoc | 32 ++++ src/reference/asciidoc/whats-new.adoc | 3 + 6 files changed, 271 insertions(+), 6 deletions(-) create mode 100644 spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/OptionalPayloadTests.java diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java index 5c102712d1..14f07da963 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java @@ -21,6 +21,7 @@ import java.lang.reflect.Type; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.Optional; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -96,6 +97,8 @@ public abstract class AbstractJackson2MessageConverter extends AbstractMessageCo private boolean alwaysConvertToInferredType; + private boolean nullAsOptionalEmpty; + /** * Construct with the provided {@link ObjectMapper} instance. * @param objectMapper the {@link ObjectMapper} to use. @@ -148,6 +151,15 @@ public void setSupportedContentType(MimeType supportedContentType) { this.supportedCTCharset = this.supportedContentType.getParameter("charset"); } + /** + * When true, if jackson decodes the body as {@code null} convert to {@link Optional#empty()} + * instead of returning the original body. Default false. + * @param nullAsOptionalEmpty true to return empty. + * @since 2.4.7 + */ + public void setNullAsOptionalEmpty(boolean nullAsOptionalEmpty) { + this.nullAsOptionalEmpty = nullAsOptionalEmpty; + } @Nullable public ClassMapper getClassMapper() { @@ -316,7 +328,12 @@ public Object fromMessage(Message message, @Nullable Object conversionHint) thro } } if (content == null) { - content = message.getBody(); + if (this.nullAsOptionalEmpty) { + content = Optional.empty(); + } + else { + content = message.getBody(); + } } return content; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java index e72f0a85fb..29379b6c44 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java @@ -18,6 +18,7 @@ import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; +import java.lang.reflect.Type; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -28,6 +29,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -73,6 +75,7 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.context.EnvironmentAware; import org.springframework.context.expression.StandardBeanExpressionResolver; +import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.MergedAnnotations; @@ -82,9 +85,14 @@ import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.env.Environment; import org.springframework.core.task.TaskExecutor; +import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.lang.Nullable; +import org.springframework.messaging.Message; +import org.springframework.messaging.converter.GenericMessageConverter; import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory; import org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory; +import org.springframework.messaging.handler.annotation.support.MethodArgumentNotValidException; +import org.springframework.messaging.handler.annotation.support.PayloadMethodArgumentResolver; import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; import org.springframework.util.Assert; @@ -92,6 +100,7 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; +import org.springframework.validation.ObjectError; import org.springframework.validation.Validator; /** @@ -980,6 +989,9 @@ private String resolve(String value) { */ private class RabbitHandlerMethodFactoryAdapter implements MessageHandlerMethodFactory { + private final DefaultFormattingConversionService defaultFormattingConversionService = + new DefaultFormattingConversionService(); + private MessageHandlerMethodFactory factory; RabbitHandlerMethodFactoryAdapter() { @@ -1008,20 +1020,70 @@ private MessageHandlerMethodFactory createDefaultMessageHandlerMethodFactory() { defaultFactory.setValidator(validator); } defaultFactory.setBeanFactory(RabbitListenerAnnotationBeanPostProcessor.this.beanFactory); - DefaultConversionService conversionService = new DefaultConversionService(); - conversionService.addConverter( + this.defaultFormattingConversionService.addConverter( new BytesToStringConverter(RabbitListenerAnnotationBeanPostProcessor.this.charset)); - defaultFactory.setConversionService(conversionService); + defaultFactory.setConversionService(this.defaultFormattingConversionService); - List customArgumentsResolver = - new ArrayList<>(RabbitListenerAnnotationBeanPostProcessor.this.registrar.getCustomMethodArgumentResolvers()); + List customArgumentsResolver = new ArrayList<>( + RabbitListenerAnnotationBeanPostProcessor.this.registrar.getCustomMethodArgumentResolvers()); defaultFactory.setCustomArgumentResolvers(customArgumentsResolver); + GenericMessageConverter messageConverter = new GenericMessageConverter( + this.defaultFormattingConversionService); + defaultFactory.setMessageConverter(messageConverter); + // Has to be at the end - look at PayloadMethodArgumentResolver documentation + customArgumentsResolver.add(new OptionalEmptyAwarePayloadArgumentResolver(messageConverter, validator)); defaultFactory.afterPropertiesSet(); return defaultFactory; } } + private static class OptionalEmptyAwarePayloadArgumentResolver extends PayloadMethodArgumentResolver { + + OptionalEmptyAwarePayloadArgumentResolver( + org.springframework.messaging.converter.MessageConverter messageConverter, + @Nullable Validator validator) { + + super(messageConverter, validator); + } + + @Override + public Object resolveArgument(MethodParameter parameter, Message message) throws Exception { // NOSONAR + Object resolved = null; + try { + resolved = super.resolveArgument(parameter, message); + } + catch (MethodArgumentNotValidException ex) { + if (message.getPayload().equals(Optional.empty())) { + Type type = parameter.getGenericParameterType(); + List allErrors = ex.getBindingResult().getAllErrors(); + if (allErrors.size() == 1 + && allErrors.get(0).getDefaultMessage().equals("Payload value must not be empty")) { + return Optional.empty(); + } + } + throw ex; + } + /* + * Replace Optional.empty() list elements with null. + */ + if (resolved instanceof List) { + List list = ((List) resolved); + for (int i = 0; i < list.size(); i++) { + if (list.get(i).equals(Optional.empty())) { + list.set(i, null); + } + } + } + return resolved; + } + + @Override + protected boolean isEmptyPayload(Object payload) { + return payload == null || payload.equals(Optional.empty()); + } + + } /** * The metadata holder of the class with {@link RabbitListener} * and {@link RabbitHandler} annotations. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java index 41c3ebdcd8..f7acd82e2c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java @@ -22,6 +22,7 @@ import java.lang.reflect.WildcardType; import java.util.Collection; import java.util.List; +import java.util.Optional; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.rabbit.listener.api.RabbitListenerErrorHandler; @@ -412,7 +413,15 @@ private Type determineInferredType() { // NOSONAR - complexity } } } + return checkOptional(genericParameterType); + } + + protected Type checkOptional(Type genericParameterType) { + if (genericParameterType instanceof ParameterizedType + && ((ParameterizedType) genericParameterType).getRawType().equals(Optional.class)) { + return ((ParameterizedType) genericParameterType).getActualTypeArguments()[0]; + } return genericParameterType; } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/OptionalPayloadTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/OptionalPayloadTests.java new file mode 100644 index 0000000000..1d5a476433 --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/OptionalPayloadTests.java @@ -0,0 +1,142 @@ +/* + * 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.annotation; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.AmqpException; +import org.springframework.amqp.core.MessageBuilder; +import org.springframework.amqp.core.MessagePropertiesBuilder; +import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.junit.RabbitAvailable; +import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * @author Gary Russell + * @since 2.8 + * + */ +@SpringJUnitConfig +@RabbitAvailable(queues = { "op.1", "op.2" }) +public class OptionalPayloadTests { + + @Test + void optionals(@Autowired RabbitTemplate template, @Autowired Listener listener) + throws JsonProcessingException, AmqpException, InterruptedException { + + ObjectMapper objectMapper = new ObjectMapper(); + template.send("op.1", MessageBuilder.withBody(objectMapper.writeValueAsBytes("foo")) + .andProperties(MessagePropertiesBuilder.newInstance() + .setContentType("application/json") + .build()) + .build()); + template.send("op.1", MessageBuilder.withBody(objectMapper.writeValueAsBytes(null)) + .andProperties(MessagePropertiesBuilder.newInstance() + .setContentType("application/json") + .build()) + .build()); + template.send("op.2", MessageBuilder.withBody(objectMapper.writeValueAsBytes("bar")) + .andProperties(MessagePropertiesBuilder.newInstance() + .setContentType("application/json") + .build()) + .build()); + template.send("op.2", MessageBuilder.withBody(objectMapper.writeValueAsBytes(null)) + .andProperties(MessagePropertiesBuilder.newInstance() + .setContentType("application/json") + .build()) + .build()); + assertThat(listener.latch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(listener.deOptionaled).containsExactlyInAnyOrder("foo", null, "bar", "baz"); + } + + @Configuration + @EnableRabbit + public static class Config { + + @Bean + RabbitTemplate template() { + return new RabbitTemplate(rabbitConnectionFactory()); + } + + @Bean + SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(rabbitConnectionFactory()); + factory.setMessageConverter(converter()); + return factory; + } + + @Bean + ConnectionFactory rabbitConnectionFactory() { + return new CachingConnectionFactory(RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); + } + + @Bean + Jackson2JsonMessageConverter converter() { + Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter(); + converter.setNullAsOptionalEmpty(true); + return converter; + } + + @Bean + Listener listener() { + return new Listener(); + } + + } + + static class Listener { + + final CountDownLatch latch = new CountDownLatch(4); + + List deOptionaled = new ArrayList<>(); + + @RabbitListener(queues = "op.1") + void listen(@Payload(required = false) String payload) { + this.deOptionaled.add(payload); + this.latch.countDown(); + } + + @RabbitListener(queues = "op.2") + void listen(Optional optional) { + this.deOptionaled.add(optional.orElse("baz")); + this.latch.countDown(); + } + + } + +} diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index 8cf2ae89c7..86c73dce52 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -4081,6 +4081,38 @@ converter to determine the type. IMPORTANT: Starting with version 1.6.11, `Jackson2JsonMessageConverter` and, therefore, `DefaultJackson2JavaTypeMapper` (`DefaultClassMapper`) provide the `trustedPackages` option to overcome https://pivotal.io/security/cve-2017-4995[Serialization Gadgets] vulnerability. By default and for backward compatibility, the `Jackson2JsonMessageConverter` trusts all packages -- that is, it uses `*` for the option. +Starting with version 2.4.7, the converter can be configured to return `Optional.empty()` if Jackson returns `null` after deserializing the message body. +This facilitates `@RabbitListener` s to receive null payloads, in two ways: + +==== +[source, java] +---- +@RabbitListener(queues = "op.1") +void listen(@Payload(required = false) Thing payload) { + handleOptional(payload); // payload might be null +} + +@RabbitListener(queues = "op.2") +void listen(Optional optional) { + handleOptional(optional.orElse(this.emptyThing)); +} +---- +==== + +To enable this feature, set `setNullAsOptionalEmpty` to `true`; when `false` (default), the converter falls back to the raw message body (`byte[]`). + +==== +[source, java] +---- +@Bean +Jackson2JsonMessageConverter converter() { + Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter(); + converter.setNullAsOptionalEmpty(true); + return converter; +} +---- +==== + [[jackson-abstract]] ====== Deserializing Abstract Classes diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index b32794367d..0fbd0fcb2e 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -28,3 +28,6 @@ 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 <> for more infoprmation. + +`MessageConverter` s can now return `Optional.empty()` for a null value; this is currently implemented by the `Jackson2JsonMessageConverter`. +See <> for more information. From 0a6a60638c7821e725398f35c453d8200cc04770 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 10 Aug 2022 09:37:23 -0400 Subject: [PATCH 131/737] GH-1491: Fix Possible NPE --- ...abbitListenerAnnotationBeanPostProcessor.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java index 29379b6c44..a7d72cbf3f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java @@ -18,7 +18,6 @@ import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; -import java.lang.reflect.Type; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -100,6 +99,7 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; +import org.springframework.validation.BindingResult; import org.springframework.validation.ObjectError; import org.springframework.validation.Validator; @@ -1055,11 +1055,15 @@ public Object resolveArgument(MethodParameter parameter, Message message) thr } catch (MethodArgumentNotValidException ex) { if (message.getPayload().equals(Optional.empty())) { - Type type = parameter.getGenericParameterType(); - List allErrors = ex.getBindingResult().getAllErrors(); - if (allErrors.size() == 1 - && allErrors.get(0).getDefaultMessage().equals("Payload value must not be empty")) { - return Optional.empty(); + BindingResult bindingResult = ex.getBindingResult(); + if (bindingResult != null) { + List allErrors = bindingResult.getAllErrors(); + if (allErrors.size() == 1) { + String defaultMessage = allErrors.get(0).getDefaultMessage(); + if ("Payload value must not be empty".equals(defaultMessage)) { + return Optional.empty(); + } + } } } throw ex; From c67f80b0de8c4739ed163fbcdfd6aaefe46097e7 Mon Sep 17 00:00:00 2001 From: renyansongno1 <45755446+renyansongno1@users.noreply.github.com> Date: Wed, 10 Aug 2022 22:40:20 +0800 Subject: [PATCH 132/737] GH-1487: Countdown not active AsyncMProcConsumer Fixes https://github.com/spring-projects/spring-amqp/issues/1487 * SimpleMessageListenerContainer.AsyncMessageProcessingConsumer countdown when the container is not active * test for AsyncMessageProcessingConsumer countdown when the container is not active * change TestExecutor for the static and final **Cherry-pick to `2.4.x`** --- .../SimpleMessageListenerContainer.java | 2 + .../SimpleMessageListenerContainerTests.java | 57 ++++++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java index 201a371dba..4bfef6db04 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java @@ -78,6 +78,7 @@ * @author Artem Bilan * @author Alex Panchenko * @author Mat Jaggard + * @author Yansong Ren * * @since 1.0 */ @@ -1192,6 +1193,7 @@ private FatalListenerStartupException getStartupException() throws InterruptedEx @Override // NOSONAR - complexity - many catch blocks public void run() { // NOSONAR - line count if (!isActive()) { + this.start.countDown(); return; } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java index c5f09aaadc..1b7d7d1229 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -86,6 +86,7 @@ import org.springframework.amqp.utils.test.TestUtils; import org.springframework.aop.support.AopUtils; import org.springframework.beans.DirectFieldAccessor; +import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionException; @@ -106,6 +107,7 @@ * @author Gary Russell * @author Artem Bilan * @author Mohammad Hewedy + * @author Yansong Ren */ public class SimpleMessageListenerContainerTests { @@ -731,6 +733,30 @@ void filterMppNoDoubleAck() throws Exception { verifyNoMoreInteractions(listener); } + @Test + void testWithConsumerStartWhenNotActive() { + ConnectionFactory connectionFactory = mock(ConnectionFactory.class); + Connection connection = mock(Connection.class); + Channel channel = mock(Channel.class); + given(connectionFactory.createConnection()).willReturn(connection); + given(connection.createChannel(false)).willReturn(channel); + + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); + // overwrite task execute. shutdown container before task execute. + TestExecutor testExecutor = new TestExecutor(container); + container.setTaskExecutor(testExecutor); + container.start(); + + // then add queue for trigger container shutdown + container.addQueueNames("bar"); + + // valid the 'start' countdown is 0. lastTask is AsyncMessageProcessingConsumer + Runnable lastTask = testExecutor.getLastTask(); + CountDownLatch start = TestUtils.getPropertyValue(lastTask, "start", CountDownLatch.class); + + assertThat(start.getCount()).isEqualTo(0L); + } + private Answer messageToConsumer(final Channel mockChannel, final SimpleMessageListenerContainer container, final boolean cancel, final CountDownLatch latch) { return invocation -> { @@ -784,4 +810,33 @@ protected void doRollback(DefaultTransactionStatus status) throws TransactionExc } + @SuppressWarnings("serial") + private static final class TestExecutor extends SimpleAsyncTaskExecutor { + + private final SimpleMessageListenerContainer simpleMessageListenerContainer; + + private int shutdownCount = 0; + + private Runnable lastTask = null; + + private TestExecutor(SimpleMessageListenerContainer simpleMessageListenerContainer) { + this.simpleMessageListenerContainer = simpleMessageListenerContainer; + } + + public Runnable getLastTask() { + return lastTask; + } + + @Override + public void execute(Runnable task) { + // skip the first execution + if (++shutdownCount > 1) { + lastTask = task; + // before execute, shutdown the container for test + this.simpleMessageListenerContainer.shutdown(); + } + super.execute(task); + } + } + } From f2e1e983170fefc217393801a2a0486d2d53b928 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 10 Aug 2022 12:23:13 -0400 Subject: [PATCH 133/737] GH-1491: Fix Fallback When Parameter is Optional Only pass `Optional.empty()` if the method parameter is `Optional`. --- .../RabbitListenerAnnotationBeanPostProcessor.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java index a7d72cbf3f..0ba855e6e3 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java @@ -18,6 +18,8 @@ import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -1054,7 +1056,8 @@ public Object resolveArgument(MethodParameter parameter, Message message) thr resolved = super.resolveArgument(parameter, message); } catch (MethodArgumentNotValidException ex) { - if (message.getPayload().equals(Optional.empty())) { + Type type = parameter.getGenericParameterType(); + if (isOptional(message, type)) { BindingResult bindingResult = ex.getBindingResult(); if (bindingResult != null) { List allErrors = bindingResult.getAllErrors(); @@ -1082,6 +1085,12 @@ public Object resolveArgument(MethodParameter parameter, Message message) thr return resolved; } + private boolean isOptional(Message message, Type type) { + return (Optional.class.equals(type) || (type instanceof ParameterizedType + && Optional.class.equals(((ParameterizedType) type).getRawType()))) + && message.getPayload().equals(Optional.empty()); + } + @Override protected boolean isEmptyPayload(Object payload) { return payload == null || payload.equals(Optional.empty()); From c76c671e7c6b46e94e4e0c84a6b6220d81d27655 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 17 Aug 2022 16:41:41 -0400 Subject: [PATCH 134/737] AOT RuntimeHints Polishing - rename hints registrar according to convention - use `registerSynthesizedAnnotation()` --- ...bbitRuntimeHintsRegistrar.java => RabbitRuntimeHints.java} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/{RabbitRuntimeHintsRegistrar.java => RabbitRuntimeHints.java} (93%) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/RabbitRuntimeHintsRegistrar.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/RabbitRuntimeHints.java similarity index 93% rename from spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/RabbitRuntimeHintsRegistrar.java rename to spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/RabbitRuntimeHints.java index f5c69b2a7d..0bb96a97ee 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/RabbitRuntimeHintsRegistrar.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/RabbitRuntimeHints.java @@ -36,11 +36,11 @@ * @since 3.0 * */ -public class RabbitRuntimeHintsRegistrar implements RuntimeHintsRegistrar { +public class RabbitRuntimeHints implements RuntimeHintsRegistrar { @Override public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { - RuntimeHintsUtils.registerAnnotation(hints, RabbitListener.class); + RuntimeHintsUtils.registerSynthesizedAnnotation(hints, RabbitListener.class); ProxyHints proxyHints = hints.proxies(); proxyHints.registerJdkProxy(ChannelProxy.class); proxyHints.registerJdkProxy(ChannelProxy.class, PublisherCallbackChannel.class); From 8c37c2d8de5c202f5cb1ed839fd4ef0087efb2f4 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 18 Aug 2022 11:08:28 -0400 Subject: [PATCH 135/737] Fix aot.factories for Previous Commit --- spring-rabbit/src/main/resources/META-INF/spring/aot.factories | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-rabbit/src/main/resources/META-INF/spring/aot.factories b/spring-rabbit/src/main/resources/META-INF/spring/aot.factories index 77cfb34d6c..eb3d20bd46 100644 --- a/spring-rabbit/src/main/resources/META-INF/spring/aot.factories +++ b/spring-rabbit/src/main/resources/META-INF/spring/aot.factories @@ -1 +1 @@ -org.springframework.aot.hint.RuntimeHintsRegistrar=org.springframework.amqp.rabbit.aot.RabbitRuntimeHintsRegistrar +org.springframework.aot.hint.RuntimeHintsRegistrar=org.springframework.amqp.rabbit.aot.RabbitRuntimeHints From 97644e95e014157ced925c2a8d8ec3cc689d425b Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 22 Aug 2022 14:11:57 -0400 Subject: [PATCH 136/737] Improve Stream Template Test Coverage --- .../producer/RabbitStreamTemplateTests.java | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplateTests.java diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplateTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplateTests.java new file mode 100644 index 0000000000..b0bb26414d --- /dev/null +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplateTests.java @@ -0,0 +1,117 @@ +/* + * 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.rabbit.stream.producer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.support.converter.SimpleMessageConverter; +import org.springframework.rabbit.stream.support.converter.StreamMessageConverter; + +import com.rabbitmq.stream.ConfirmationHandler; +import com.rabbitmq.stream.ConfirmationStatus; +import com.rabbitmq.stream.Constants; +import com.rabbitmq.stream.Environment; +import com.rabbitmq.stream.Message; +import com.rabbitmq.stream.Producer; +import com.rabbitmq.stream.ProducerBuilder; + +/** + * @author Gary Russell + * @since 2.4.7 + * + */ +public class RabbitStreamTemplateTests { + + @Test + void handleConfirm() throws InterruptedException, ExecutionException { + Environment env = mock(Environment.class); + ProducerBuilder pb = mock(ProducerBuilder.class); + given(env.producerBuilder()).willReturn(pb); + Producer producer = mock(Producer.class); + given(pb.build()).willReturn(producer); + AtomicInteger which = new AtomicInteger(); + willAnswer(inv -> { + ConfirmationHandler handler = inv.getArgument(1); + ConfirmationStatus status = null; + switch (which.getAndIncrement()) { + case 0: + status = new ConfirmationStatus(inv.getArgument(0), true, (short) 0); + break; + case 1: + status = new ConfirmationStatus(inv.getArgument(0), false, Constants.CODE_MESSAGE_ENQUEUEING_FAILED); + break; + case 2: + status = new ConfirmationStatus(inv.getArgument(0), false, Constants.CODE_PRODUCER_CLOSED); + break; + case 3: + status = new ConfirmationStatus(inv.getArgument(0), false, Constants.CODE_PRODUCER_NOT_AVAILABLE); + break; + case 4: + status = new ConfirmationStatus(inv.getArgument(0), false, Constants.CODE_PUBLISH_CONFIRM_TIMEOUT); + break; + case 5: + status = new ConfirmationStatus(inv.getArgument(0), false, (short) -1); + break; + } + handler.handle(status); + return null; + }).given(producer).send(any(), any()); + try (RabbitStreamTemplate template = new RabbitStreamTemplate(env, "foo")) { + SimpleMessageConverter messageConverter = new SimpleMessageConverter(); + template.setMessageConverter(messageConverter); + assertThat(template.messageConverter()).isSameAs(messageConverter); + StreamMessageConverter converter = mock(StreamMessageConverter.class); + given(converter.fromMessage(any())).willReturn(mock(Message.class)); + template.setStreamConverter(converter); + assertThat(template.streamMessageConverter()).isSameAs(converter); + CompletableFuture future = template.convertAndSend("foo"); + assertThat(future.get()).isTrue(); + CompletableFuture future1 = template.convertAndSend("foo"); + assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> future1.get()) + .withCauseExactlyInstanceOf(StreamSendException.class) + .withStackTraceContaining("Message Enqueueing Failed"); + CompletableFuture future2 = template.convertAndSend("foo"); + assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> future2.get()) + .withCauseExactlyInstanceOf(StreamSendException.class) + .withStackTraceContaining("Producer Closed"); + CompletableFuture future3 = template.convertAndSend("foo"); + assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> future3.get()) + .withCauseExactlyInstanceOf(StreamSendException.class) + .withStackTraceContaining("Producer Not Available"); + CompletableFuture future4 = template.convertAndSend("foo"); + assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> future4.get()) + .withCauseExactlyInstanceOf(StreamSendException.class) + .withStackTraceContaining("Publish Confirm Timeout"); + CompletableFuture future5 = template.convertAndSend("foo"); + assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> future5.get()) + .withCauseExactlyInstanceOf(StreamSendException.class) + .withStackTraceContaining("Unknown code: " + -1); + } + } + +} From ab3eb36a47a9d9c2597d4ba2fba63ef5e056da37 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 22 Aug 2022 12:15:23 -0400 Subject: [PATCH 137/737] GH-1494: Fix Test Harness with @Repeatable Resolves https://github.com/spring-projects/spring-amqp/issues/1494 Capture mode failed to capture arguments/result/exception if multiple `@RabbitListener` annotations present. **cherry-pick to 2.4.x** --- .../amqp/rabbit/test/RabbitListenerTestHarness.java | 10 +++++----- .../examples/ExampleRabbitListenerCaptureTest.java | 8 +++++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/RabbitListenerTestHarness.java b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/RabbitListenerTestHarness.java index 2c8c0e2147..05adc02496 100644 --- a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/RabbitListenerTestHarness.java +++ b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/RabbitListenerTestHarness.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-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. @@ -41,7 +41,7 @@ import org.springframework.amqp.rabbit.test.mockito.LatchCountDownAndCallRealMethodAnswer; import org.springframework.aop.framework.ProxyFactoryBean; import org.springframework.core.annotation.AnnotationAttributes; -import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.type.AnnotationMetadata; import org.springframework.test.util.AopTestUtils; import org.springframework.util.Assert; @@ -172,9 +172,9 @@ private static final class CaptureAdvice implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { - boolean isListenerMethod = - AnnotationUtils.findAnnotation(invocation.getMethod(), RabbitListener.class) != null - || AnnotationUtils.findAnnotation(invocation.getMethod(), RabbitHandler.class) != null; + MergedAnnotations annotations = MergedAnnotations.from(invocation.getMethod()); + boolean isListenerMethod = annotations.isPresent(RabbitListener.class) + || annotations.isPresent(RabbitHandler.class); try { Object result = invocation.proceed(); if (isListenerMethod) { diff --git a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerCaptureTest.java b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerCaptureTest.java index a70becefdd..2be262a488 100644 --- a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerCaptureTest.java +++ b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerCaptureTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-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. @@ -139,6 +139,11 @@ public Queue queue2() { return new AnonymousQueue(); } + @Bean + public Queue queue3() { + return new AnonymousQueue(); + } + @Bean public RabbitAdmin admin(ConnectionFactory cf) { return new RabbitAdmin(cf); @@ -168,6 +173,7 @@ public String foo(String foo) { } @RabbitListener(id = "bar", queues = "#{queue2.name}") + @RabbitListener(id = "bar2", queues = "#{queue3.name}") public void foo(@Payload String foo, @Header("amqp_receivedRoutingKey") String rk) { if (!failed && foo.equals("ex")) { failed = true; From 7e4701446ed977dc740cc4e9315457d46e3f4f2d Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 30 Aug 2022 14:09:42 -0400 Subject: [PATCH 138/737] Move to Micrometer Snapshots --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index f2f2310386..6524f7f4d0 100644 --- a/build.gradle +++ b/build.gradle @@ -55,8 +55,8 @@ ext { log4jVersion = '2.17.2' logbackVersion = '1.2.3' lz4Version = '1.8.0' - micrometerVersion = '1.10.0-M3' - micrometerTracingVersion = '1.0.0-M6' + micrometerVersion = '1.10.0-SNAPSHOT' + micrometerTracingVersion = '1.0.0-SNAPSHOT' mockitoVersion = '4.5.1' rabbitmqStreamVersion = '0.4.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.13.1' From 5e6595d964033a640582fc64c368453c24ffe074 Mon Sep 17 00:00:00 2001 From: Jay Bryant Date: Thu, 1 Sep 2022 11:05:55 -0500 Subject: [PATCH 139/737] Switch to spring-asciidoctor-backends to get the new look and feel and the new features. --- build.gradle | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index 6524f7f4d0..a74a9fe319 100644 --- a/build.gradle +++ b/build.gradle @@ -34,7 +34,7 @@ ext { linkScmUrl = 'https://github.com/spring-projects/spring-amqp' linkScmConnection = 'git://github.com/spring-projects/spring-amqp.git' linkScmDevConnection = 'git@github.com:spring-projects/spring-amqp.git' - docResourcesVersion = '0.2.1.RELEASE' + springAsciidoctorBackendsVersion = '0.0.3' modifiedFiles = files(grgit.status().unstaged.modified).filter{ f -> f.name.endsWith('.java') || f.name.endsWith('.kt') } @@ -487,18 +487,15 @@ project('spring-rabbit-test') { } configurations { - docs + asciidoctorExtensions } dependencies { - docs "io.spring.docresources:spring-doc-resources:${docResourcesVersion}@zip" + asciidoctorExtensions "io.spring.asciidoctor.backends:spring-asciidoctor-backends:${springAsciidoctorBackendsVersion}" } task prepareAsciidocBuild(type: Sync) { - dependsOn configurations.docs - from { - configurations.docs.collect { zipTree(it) } - } + dependsOn configurations.asciidoctorExtensions duplicatesStrategy = DuplicatesStrategy.EXCLUDE from 'src/reference/asciidoc/' into "$buildDir/asciidoc" @@ -529,6 +526,10 @@ asciidoctor { dependsOn asciidoctorPdf baseDirFollowsSourceFile() sourceDir "$buildDir/asciidoc" + configurations 'asciidoctorExtensions' + outputOptions { + backends "spring-html" + } resources { from(sourceDir) { include 'images/*', 'css/**', 'js/**' From c929cd5d35b46037390ee2d57c98614434cba202 Mon Sep 17 00:00:00 2001 From: Ruben Vervaeke Date: Fri, 26 Aug 2022 17:59:11 +0200 Subject: [PATCH 140/737] GH-1497: Use RANDOM as default addressShuffleMode Resolves https://github.com/spring-projects/spring-amqp/issues/1497 GH-1497: Remove deprecated setShuffleAddresses method GH-1497: Fix failing setAddressesTwoHosts Doc Polishing. --- .../connection/AbstractConnectionFactory.java | 19 ++----------------- .../CachingConnectionFactoryTests.java | 10 +++++++--- src/reference/asciidoc/amqp.adoc | 9 +++++---- src/reference/asciidoc/whats-new.adoc | 5 +++++ 4 files changed, 19 insertions(+), 24 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java index cc0d1c67be..f17411c9dc 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -139,7 +139,7 @@ public void handleRecovery(Recoverable recoverable) { private List
addresses; - private AddressShuffleMode addressShuffleMode = AddressShuffleMode.NONE; + private AddressShuffleMode addressShuffleMode = AddressShuffleMode.RANDOM; private int closeTimeout = DEFAULT_CLOSE_TIMEOUT; @@ -523,21 +523,6 @@ protected String getBeanName() { return this.beanName; } - /** - * When {@link #setAddresses(String) addresses} are provided and there is more than - * one, set to true to shuffle the list before opening a new connection so that the - * connection to the broker will be attempted in random order. - * @param shuffleAddresses true to shuffle the list. - * @since 2.1.8 - * @deprecated since 2.3 in favor of - * @see Collections#shuffle(List) - * {@link #setAddressShuffleMode(AddressShuffleMode)}. - */ - @Deprecated - public void setShuffleAddresses(boolean shuffleAddresses) { - setAddressShuffleMode(AddressShuffleMode.RANDOM); - } - /** * Set the mode for shuffling addresses. * @param addressShuffleMode the address shuffle mode. diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java index 7534b46565..5126ea9a17 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -24,6 +24,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.BDDMockito.given; @@ -65,6 +66,7 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; import org.mockito.InOrder; import org.springframework.amqp.AmqpConnectException; @@ -1657,8 +1659,10 @@ public void setAddressesTwoHosts() throws Exception { ccf.createConnection(); verify(mock).isAutomaticRecoveryEnabled(); verify(mock).setAutomaticRecoveryEnabled(false); - verify(mock).newConnection(isNull(), - eq(Arrays.asList(new Address("mq1"), new Address("mq2"))), anyString()); + verify(mock).newConnection( + isNull(), + argThat((ArgumentMatcher>) a -> a.size() == 2 && a.contains(new Address("mq1")) && a.contains(new Address("mq2"))), + anyString()); verifyNoMoreInteractions(mock); } diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index 86c73dce52..0d4931b61e 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -609,9 +609,10 @@ public CachingConnectionFactory ccf() { ---- ==== -The underlying connection factory will attempt to connect to each host, in order, whenever a new connection is established. -Starting with version 2.1.8, the connection order can be made random by setting the `addressShuffleMode` property to `RANDOM`; the shuffle will be applied before creating any new connection. -Starting with version 2.6, the `INORDER` shuffle mode was added, which means the first address is moved to the end after a connection is created. +Starting with version 3.0, the underlying connection factory will attempt to connect to a host, by choosing a random address, whenever a new connection is established. +To revert to the previous behavior of attempting to connect from first to last, set the `addressShuffleMode` property to `AddressShuffleMode.NONE`. + +Starting with version 2.3, the `INORDER` shuffle mode was added, which means the first address is moved to the end after a connection is created. You may wish to use this mode with the https://github.com/rabbitmq/rabbitmq-sharding[RabbitMQ Sharding Plugin] with `CacheMode.CONNECTION` and suitable concurrency if you wish to consume from all shards on all nodes. ==== @@ -621,7 +622,7 @@ You may wish to use this mode with the https://github.com/rabbitmq/rabbitmq-shar public CachingConnectionFactory ccf() { CachingConnectionFactory ccf = new CachingConnectionFactory(); ccf.setAddresses("host1:5672,host2:5672,host3:5672"); - ccf.setAddressShuffleMode(AddressShuffleMode.RANDOM); + ccf.setAddressShuffleMode(AddressShuffleMode.INORDER); return ccf; } ---- diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index 0fbd0fcb2e..6825e4b558 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -31,3 +31,8 @@ See <> for more infoprmation. `MessageConverter` s can now return `Optional.empty()` for a null value; this is currently implemented by the `Jackson2JsonMessageConverter`. See <> for more information. + +==== Connection Factory Changes + +The default `addressShuffleMode` in `AbstractConnectionFactory` is now `RANDOM`. This results in connecting to a random host when multiple addresses are provided. +See <> for more information. \ No newline at end of file From 23076f8c80dba3af44837f4ca598ddff55772bb7 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 6 Sep 2022 10:54:52 -0400 Subject: [PATCH 141/737] GH-1497: Fix Test --- .../rabbit/connection/AbstractConnectionFactoryTests.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactoryTests.java index 3fc935d5cb..cab3e670f9 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2020 the original author or authors. + * Copyright 2010-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. @@ -39,6 +39,7 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +import org.springframework.amqp.rabbit.connection.AbstractConnectionFactory.AddressShuffleMode; import org.springframework.amqp.utils.test.TestUtils; import org.springframework.beans.DirectFieldAccessor; import org.springframework.scheduling.concurrent.CustomizableThreadFactory; @@ -104,6 +105,7 @@ public void onClose(Connection connection) { verify(mockConnectionFactory, times(1)).newConnection(any(ExecutorService.class), anyString()); connectionFactory.setAddresses("foo:5672,bar:5672"); + connectionFactory.setAddressShuffleMode(AddressShuffleMode.NONE); con = connectionFactory.createConnection(); assertThat(called.get()).isEqualTo(1); captor = ArgumentCaptor.forClass(String.class); From f2717a184b4f72648c3e758ec346892741487c22 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 6 Sep 2022 12:18:53 -0400 Subject: [PATCH 142/737] Remove RHU.registerSynthesizedAnnotation() --- .../springframework/amqp/rabbit/aot/RabbitRuntimeHints.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/RabbitRuntimeHints.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/RabbitRuntimeHints.java index 0bb96a97ee..9b4bea3b9b 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/RabbitRuntimeHints.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/RabbitRuntimeHints.java @@ -16,7 +16,6 @@ package org.springframework.amqp.rabbit.aot; -import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.rabbit.connection.ChannelProxy; import org.springframework.amqp.rabbit.connection.PublisherCallbackChannel; import org.springframework.aop.SpringProxy; @@ -25,7 +24,6 @@ import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.TypeReference; -import org.springframework.aot.hint.support.RuntimeHintsUtils; import org.springframework.core.DecoratingProxy; import org.springframework.lang.Nullable; @@ -40,7 +38,6 @@ public class RabbitRuntimeHints implements RuntimeHintsRegistrar { @Override public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { - RuntimeHintsUtils.registerSynthesizedAnnotation(hints, RabbitListener.class); ProxyHints proxyHints = hints.proxies(); proxyHints.registerJdkProxy(ChannelProxy.class); proxyHints.registerJdkProxy(ChannelProxy.class, PublisherCallbackChannel.class); From ba4b62e255089165611e2482a4d20e757088963b Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 6 Sep 2022 15:15:11 -0400 Subject: [PATCH 143/737] GH-1449: Fix Auto Recovery Docs Resolves https://github.com/spring-projects/spring-amqp/issues/1449 --- src/reference/asciidoc/amqp.adoc | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index 0d4931b61e..dc011b1666 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -987,14 +987,12 @@ image::images/cacheStats.png[align="center"] Since the first version of Spring AMQP, the framework has provided its own connection and channel recovery in the event of a broker failure. Also, as discussed in <>, the `RabbitAdmin` re-declares any infrastructure beans (queues and others) when the connection is re-established. It therefore does not rely on the https://www.rabbitmq.com/api-guide.html#recovery[auto-recovery] that is now provided by the `amqp-client` library. -Spring AMQP now uses the `4.0.x` version of `amqp-client`, which has auto recovery enabled by default. -Spring AMQP can still use its own recovery mechanisms if you wish, disabling it in the client, (by setting the `automaticRecoveryEnabled` property on the underlying `RabbitMQ connectionFactory` to `false`). -However, the framework is completely compatible with auto-recovery being enabled. -This means any consumers you create within your code (perhaps via `RabbitTemplate.execute()`) can be recovered automatically. +The `amqp-client`, has auto recovery enabled by default. +There are some incompatibilities between the two recovery mechanisms so, by default, Spring sets the `automaticRecoveryEnabled` property on the underlying `RabbitMQ connectionFactory` to `false`. +Even if the property is `true`, Spring effectively disables it, by immediately closing any recovered connections. -IMPORTANT: Only elements (queues, exchanges, bindings) that are defined as beans will be re-declared after a connection failure. -Elements declared by invoking `RabbitAdmin.declare*()` methods directly from user code are unknown to the framework and therefore cannot be recovered. -If you have a need for a variable number of declarations, consider defining a bean, or beans, of type `Declarables`, as discussed in <>. +IMPORTANT: By default, only elements (queues, exchanges, bindings) that are defined as beans will be re-declared after a connection failure. +See <> for how to change that behavior. [[custom-client-props]] ==== Adding Custom Client Connection Properties @@ -5405,7 +5403,7 @@ Normally, the `RabbitAdmin` (s) only recover queues/exchanges/bindings that are When the connection is re-established, the admin will redeclare the entities. Normally, entities created by calling `admin.declareQueue(...)`, `admin.declareExchange(...)` and `admin.declareBinding(...)` will not be recovered. -Starting with version 2.4, the admin has a new property `redeclareManualDeclarations`; when true, the admin will recover these entities in addition to the beans in the application context. +Starting with version 2.4, the admin has a new property `redeclareManualDeclarations`; when `true`, the admin will recover these entities in addition to the beans in the application context. Recovery of individual declarations will not be performed if `deleteQueue(...)`, `deleteExchange(...)` or `removeBinding(...)` is called. Associated bindings are removed from the recoverable entities when queues and exchanges are deleted. From d4e0f5c366a7ffae073f608c3766c82064cab3d1 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 6 Sep 2022 15:55:47 -0400 Subject: [PATCH 144/737] GH-1485: Remove Deprecations Resolves https://github.com/spring-projects/spring-amqp/issues/1485 --- .../core/AmqpMessageReturnedException.java | 9 +- .../springframework/amqp/core/Message.java | 12 +- .../org/springframework/amqp/core/Queue.java | 26 +--- .../amqp/core/QueueBuilder.java | 53 +-------- .../rabbit/test/mockito/LambdaAnswer.java | 13 +- ...LatchCountDownAndCallRealMethodAnswer.java | 13 +- .../connection/CachingConnectionFactory.java | 39 ------ .../rabbit/connection/CorrelationData.java | 34 +----- .../connection/PublisherCallbackChannel.java | 31 +---- .../amqp/rabbit/core/RabbitTemplate.java | 111 +----------------- .../MultiMethodRabbitListenerEndpoint.java | 13 +- ...RepublishMessageRecovererWithConfirms.java | 1 - .../PublisherCallbackChannelTests.java | 5 +- .../amqp/rabbit/core/RabbitTemplateTests.java | 22 ---- 14 files changed, 14 insertions(+), 368 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpMessageReturnedException.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpMessageReturnedException.java index 4cb710336e..d831fef822 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpMessageReturnedException.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpMessageReturnedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-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. @@ -32,13 +32,6 @@ public class AmqpMessageReturnedException extends AmqpException { private final ReturnedMessage returned; - @Deprecated - public AmqpMessageReturnedException(String message, Message returnedMessage, int replyCode, String replyText, - String exchange, String routingKey) { - - this(message, new ReturnedMessage(returnedMessage, replyCode, replyText, exchange, routingKey)); - } - public AmqpMessageReturnedException(String message, ReturnedMessage returned) { super(message); this.returned = returned; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/Message.java b/spring-amqp/src/main/java/org/springframework/amqp/core/Message.java index 32682fb950..3cb4872102 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/Message.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/Message.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -74,16 +74,6 @@ public Message(byte[] body, MessageProperties messageProperties) { //NOSONAR this.messageProperties = messageProperties; } - /** - * No longer used. - * @param patterns the patterns. - * @since 1.5.7 - * @deprecated toString() no longer deserializes the body. - */ - @Deprecated - public static void addAllowedListPatterns(String... patterns) { - } - /** * Set the encoding to use in {@link #toString()} when converting the body if * there is no {@link MessageProperties#getContentEncoding() contentEncoding} message property present. diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/Queue.java b/spring-amqp/src/main/java/org/springframework/amqp/core/Queue.java index 5cebaa1990..3ce08b21d9 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/Queue.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/Queue.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -32,14 +32,6 @@ */ public class Queue extends AbstractDeclarable implements Cloneable { - /** - * Argument key for the master locator. - * @since 2.1 - * @deprecated in favor of {@link #X_QUEUE_LEADER_LOCATOR}. - */ - @Deprecated - public static final String X_QUEUE_MASTER_LOCATOR = "x-queue-master-locator"; - /** * Argument key for the queue leader locator. * @since 2.1 @@ -165,22 +157,6 @@ public String getActualName() { return this.actualName; } - /** - * Set the master locator strategy argument for this queue. - * @param locator the locator; null to clear the argument. - * @since 2.1 - * @deprecated in favor of {@link #setLeaderLocator(String)}. - */ - @Deprecated - public final void setMasterLocator(@Nullable String locator) { - if (locator == null) { - removeArgument(X_QUEUE_LEADER_LOCATOR); - } - else { - addArgument(X_QUEUE_LEADER_LOCATOR, locator); - } - } - /** * Set the leader locator strategy argument for this queue. * @param locator the locator; null to clear the argument. diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/QueueBuilder.java b/spring-amqp/src/main/java/org/springframework/amqp/core/QueueBuilder.java index f76bbdb9d8..0b99462d35 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/QueueBuilder.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/QueueBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 the original author or authors. + * Copyright 2016-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. @@ -223,20 +223,6 @@ public QueueBuilder lazy() { return withArgument("x-queue-mode", "lazy"); } - /** - * Set the master locator mode which determines which node a queue master will be - * located on a cluster of nodes. - * @param locator {@link MasterLocator#minMasters}, {@link MasterLocator#clientLocal} - * or {@link MasterLocator#random}. - * @return the builder. - * @since 2.2 - * @deprecated in favor of {@link #leaderLocator(LeaderLocator)}. - */ - @Deprecated - public QueueBuilder masterLocator(MasterLocator locator) { - return withArgument("x-queue-master-locator", locator.getValue()); - } - /** * Set the master locator mode which determines which node a queue master will be * located on a cluster of nodes. @@ -326,43 +312,6 @@ public String getValue() { } - /** - * @deprecated in favor of {@link LeaderLocator}. - */ - @Deprecated - public enum MasterLocator { - - /** - * Deploy on the node with the fewest masters. - */ - minMasters("min-masters"), - - /** - * Deploy on the node we are connected to. - */ - clientLocal("client-local"), - - /** - * Deploy on a random node. - */ - random("random"); - - private final String value; - - MasterLocator(String value) { - this.value = value; - } - - /** - * Return the value. - * @return the value. - */ - public String getValue() { - return this.value; - } - - } - /** * Locate the queue leader. * diff --git a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LambdaAnswer.java b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LambdaAnswer.java index 191fc447f1..33dbfee5e4 100644 --- a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LambdaAnswer.java +++ b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LambdaAnswer.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-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. @@ -47,17 +47,6 @@ public class LambdaAnswer extends ForwardsInvocations { private final boolean hasDelegate; - /** - * Deprecated. - * @param callRealMethod true to call the real method. - * @param callback the callback. - * @deprecated in favor of {@link #LambdaAnswer(boolean, ValueToReturn, Object)}. - */ - @Deprecated - public LambdaAnswer(boolean callRealMethod, ValueToReturn callback) { - this(callRealMethod, callback, null); - } - /** * Construct an instance with the provided properties. Use the test harness to get an * instance with the proper delegate. diff --git a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LatchCountDownAndCallRealMethodAnswer.java b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LatchCountDownAndCallRealMethodAnswer.java index ae42862f4f..d455693706 100644 --- a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LatchCountDownAndCallRealMethodAnswer.java +++ b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LatchCountDownAndCallRealMethodAnswer.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 the original author or authors. + * Copyright 2016-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. @@ -46,17 +46,6 @@ public class LatchCountDownAndCallRealMethodAnswer extends ForwardsInvocations { private final boolean hasDelegate; - /** - * Get an instance with no delegate. - * @param count to set in a {@link CountDownLatch}. - * @deprecated in favor of - * {@link #LatchCountDownAndCallRealMethodAnswer(int, Object)}. - */ - @Deprecated - public LatchCountDownAndCallRealMethodAnswer(int count) { - this(count, null); - } - /** * Get an instance with the provided properties. Use the test harness to get an * instance with the proper delegate. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java index 78e6e5e944..6ac59839a2 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java @@ -397,45 +397,6 @@ public void setPublisherReturns(boolean publisherReturns) { } } - /** - * Use full (correlated) publisher confirms, with correlation data and a callback for - * each message. - * @param publisherConfirms true for full publisher returns, - * @since 1.1 - * @deprecated in favor of {@link #setPublisherConfirmType(ConfirmType)}. - * @see #setSimplePublisherConfirms(boolean) - */ - @Deprecated - public void setPublisherConfirms(boolean publisherConfirms) { - Assert.isTrue(!publisherConfirms || !ConfirmType.SIMPLE.equals(this.confirmType), - "Cannot set both publisherConfirms and simplePublisherConfirms"); - if (publisherConfirms) { - setPublisherConfirmType(ConfirmType.CORRELATED); - } - else if (this.confirmType.equals(ConfirmType.CORRELATED)) { - setPublisherConfirmType(ConfirmType.NONE); - } - } - - /** - * Use simple publisher confirms where the template simply waits for completion. - * @param simplePublisherConfirms true for confirms. - * @since 2.1 - * @deprecated in favor of {@link #setPublisherConfirmType(ConfirmType)}. - * @see #setPublisherConfirms(boolean) - */ - @Deprecated - public void setSimplePublisherConfirms(boolean simplePublisherConfirms) { - Assert.isTrue(!simplePublisherConfirms || !ConfirmType.CORRELATED.equals(this.confirmType), - "Cannot set both publisherConfirms and simplePublisherConfirms"); - if (simplePublisherConfirms) { - setPublisherConfirmType(ConfirmType.SIMPLE); - } - else if (this.confirmType.equals(ConfirmType.SIMPLE)) { - setPublisherConfirmType(ConfirmType.NONE); - } - } - @Override public boolean isSimplePublisherConfirms() { return this.confirmType.equals(ConfirmType.SIMPLE); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java index 46ffaedcd7..b6909d5180 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java @@ -20,7 +20,6 @@ import java.util.concurrent.CompletableFuture; import org.springframework.amqp.core.Correlation; -import org.springframework.amqp.core.Message; import org.springframework.amqp.core.ReturnedMessage; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -99,44 +98,13 @@ public CompletableFuture getFuture() { * Return a future to check the success/failure of the publish operation. * @return the future. * @since 2.4.7 - * @deprecated in favor of {@link #getFuture()}. + * @deprecated as of 3.0, in favor of {@link #getFuture()}. */ @Deprecated public CompletableFuture getCompletableFuture() { return this.future; } - /** - * Return a returned message, if any; requires a unique - * {@link #CorrelationData(String) id}. Guaranteed to be populated before the future - * is set. - * @return the message or null. - * @since 2.1 - * @deprecated in favor of {@link #getReturned()}. - */ - @Deprecated - @Nullable - public Message getReturnedMessage() { - if (this.returnedMessage == null) { - return null; - } - else { - return this.returnedMessage.getMessage(); - } - } - - /** - * Set a returned message for this correlation data. - * @param returnedMessage the returned message. - * @since 1.7.13 - * @deprecated in favor of {@link #setReturned(ReturnedMessage)}. - */ - @Deprecated - public void setReturnedMessage(Message returnedMessage) { - this.returnedMessage = new ReturnedMessage(returnedMessage, 0, "not available", "not available", - "not available"); - } - /** * Get the returned message and metadata, if any. Guaranteed to be populated before * the future is set. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannel.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannel.java index e27eba7207..7370d017e7 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannel.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannel.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -19,7 +19,6 @@ import java.util.Collection; import java.util.function.Consumer; -import com.rabbitmq.client.AMQP; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Return; @@ -115,37 +114,11 @@ interface Listener { */ void handleConfirm(PendingConfirm pendingConfirm, boolean ack); - /** - * Handle a returned message. - * @param replyCode the reply code. - * @param replyText the reply text. - * @param exchange the exchange. - * @param routingKey the routing key. - * @param properties the message properties. - * @param body the message body. - * @deprecated in favor of {@link #handleReturn(Return)}. - */ - @Deprecated - default void handleReturn(int replyCode, - String replyText, - String exchange, - String routingKey, - AMQP.BasicProperties properties, - byte[] body) { - - throw new UnsupportedOperationException( - "This should never be called; please open a GitHub issue with a stack trace"); - } - /** * Handle a returned message. * @param returned the message and metadata. */ - @SuppressWarnings("deprecation") - default void handleReturn(Return returned) { - handleReturn(returned.getReplyCode(), returned.getReplyText(), returned.getExchange(), - returned.getRoutingKey(), returned.getProperties(), returned.getBody()); - } + void handleReturn(Return returned); /** * When called, this listener should remove all references to the diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java index 3160309366..aecc38de30 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java @@ -471,29 +471,7 @@ public void setConfirmCallback(ConfirmCallback confirmCallback) { /** * Set a callback to receive returned messages. * @param returnCallback the callback. - * @deprecated in favor of {@link #setReturnsCallback(ReturnsCallback)}. */ - @Deprecated - public void setReturnCallback(ReturnCallback returnCallback) { - ReturnCallback delegate = this.returnsCallback == null ? null : this.returnsCallback.delegate(); - Assert.state(this.returnsCallback == null || delegate == null || delegate.equals(returnCallback), - "Only one ReturnCallback is supported by each RabbitTemplate"); - this.returnsCallback = new ReturnsCallback() { - - @Override - public void returnedMessage(ReturnedMessage returned) { - returnCallback.returnedMessage(returned.getMessage(), returned.getReplyCode(), returned.getReplyText(), - returned.getExchange(), returned.getRoutingKey()); - } - - @Override - public ReturnCallback delegate() { - return returnCallback; - } - - }; - } - public void setReturnsCallback(ReturnsCallback returnCallback) { Assert.state(this.returnsCallback == null || this.returnsCallback.equals(returnCallback), "Only one ReturnCallback is supported by each RabbitTemplate"); @@ -2584,18 +2562,6 @@ public void handleConfirm(PendingConfirm pendingConfirm, boolean ack) { } } - @Override - @SuppressWarnings("deprecation") - public void handleReturn(int replyCode, - String replyText, - String exchange, - String routingKey, - BasicProperties properties, - byte[] body) { - - handleReturn(new Return(replyCode, replyText, exchange, routingKey, properties, body)); - } - @Override public void handleReturn(Return returned) { ReturnsCallback callback = this.returnsCallback; @@ -2683,17 +2649,6 @@ public void onMessage(Message message, @Nullable Channel channel) { } } - /** - * {@inheritDoc} - * - * @deprecated - use {@link #onMessage(Message, Channel)}. - */ - @Deprecated - @Override - public void onMessage(Message message) { - onMessage(message, null); - } - private void restoreProperties(Message message, PendingReply pendingReply) { if (!this.userCorrelationId) { // Restore the inbound correlation data @@ -2854,82 +2809,20 @@ public interface ConfirmCallback { } - /** - * A callback for returned messages. - * - * @deprecated in favor of {@link #returnedMessage(ReturnedMessage)} which is - * easier to use with lambdas. - */ - @Deprecated - @FunctionalInterface - public interface ReturnCallback { - - /** - * Returned message callback. - * @param message the returned message. - * @param replyCode the reply code. - * @param replyText the reply text. - * @param exchange the exchange. - * @param routingKey the routing key. - */ - void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey); - - /** - * Returned message callback. - * @param returned the returned message and metadata. - */ - @SuppressWarnings("deprecation") - default void returnedMessage(ReturnedMessage returned) { - returnedMessage(returned.getMessage(), returned.getReplyCode(), returned.getReplyText(), - returned.getExchange(), returned.getRoutingKey()); - } - - } - /** * A callback for returned messages. * * @since 2.3 */ @FunctionalInterface - public interface ReturnsCallback extends ReturnCallback { - - /** - * Returned message callback. - * @param message the returned message. - * @param replyCode the reply code. - * @param replyText the reply text. - * @param exchange the exchange. - * @param routingKey the routing key. - * @deprecated in favor of {@link #returnedMessage(ReturnedMessage)} which is - * easier to use with lambdas. - */ - @Override - @Deprecated - default void returnedMessage(Message message, int replyCode, String replyText, String exchange, - String routingKey) { - - throw new UnsupportedOperationException( - "This should never be called, please open a GitHub issue with a stack trace"); - }; + public interface ReturnsCallback { /** * Returned message callback. * @param returned the returned message and metadata. */ - @Override void returnedMessage(ReturnedMessage returned); - /** - * Internal use only; transitional during deprecation. - * @return the legacy delegate. - * @deprecated - will be removed with {@link ReturnCallback}. - */ - @Deprecated - @Nullable - default ReturnCallback delegate() { - return null; - } - } + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MultiMethodRabbitListenerEndpoint.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MultiMethodRabbitListenerEndpoint.java index 8a57710aff..2f5ebbd865 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MultiMethodRabbitListenerEndpoint.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MultiMethodRabbitListenerEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2021 the original author or authors. + * Copyright 2015-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. @@ -40,17 +40,6 @@ public class MultiMethodRabbitListenerEndpoint extends MethodRabbitListenerEndpo private Validator validator; - /** - * Construct an instance for the provided methods and bean. - * @param methods the methods. - * @param bean the bean. - * @deprecated - no longer used. - */ - @Deprecated - public MultiMethodRabbitListenerEndpoint(List methods, Object bean) { - this(methods, null, bean); - } - /** * Construct an instance for the provided methods, default method and bean. * @param methods the methods. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirms.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirms.java index cbdd107bf2..65701c0c02 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirms.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirms.java @@ -108,7 +108,6 @@ protected void doSend(@Nullable } } - @SuppressWarnings("deprecation") private void doSendCorrelated(String exchange, String routingKey, Message message) { CorrelationData cd = new CorrelationData(); if (exchange != null) { diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelTests.java index e738a59476..6dcd35b9c6 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 the original author or authors. + * Copyright 2019-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. @@ -64,11 +64,10 @@ @RabbitAvailable public class PublisherCallbackChannelTests { - @SuppressWarnings("deprecation") @Test void correlationData() { CorrelationData cd = new CorrelationData(); - assertThat(cd.getReturnedMessage()).isNull(); + assertThat(cd.getReturned()).isNull(); } @Test diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateTests.java index 0c36a16228..7951a40611 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateTests.java @@ -61,7 +61,6 @@ import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.core.Queue; import org.springframework.amqp.core.ReceiveAndReplyCallback; -import org.springframework.amqp.core.ReturnedMessage; import org.springframework.amqp.rabbit.connection.AbstractRoutingConnectionFactory; import org.springframework.amqp.rabbit.connection.AfterCompletionFailedException; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; @@ -71,7 +70,6 @@ import org.springframework.amqp.rabbit.connection.RabbitUtils; import org.springframework.amqp.rabbit.connection.SimpleRoutingConnectionFactory; import org.springframework.amqp.rabbit.connection.SingleConnectionFactory; -import org.springframework.amqp.rabbit.core.RabbitTemplate.ReturnsCallback; import org.springframework.amqp.rabbit.transaction.RabbitTransactionManager; import org.springframework.amqp.support.converter.SimpleMessageConverter; import org.springframework.amqp.utils.SerializationUtils; @@ -580,26 +578,6 @@ public void testPublisherConnWithInvokeInTx() { verify(conn).createChannel(true); } - @SuppressWarnings("deprecation") - @Test - public void testReturnsFallback() { - RabbitTemplate template = new RabbitTemplate(); - AtomicBoolean called = new AtomicBoolean(); - template.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> { - called.set(true); - }); - ReturnsCallback cb = TestUtils.getPropertyValue(template, "returnsCallback", ReturnsCallback.class); - cb.returnedMessage(new ReturnedMessage(null, 0, null, null, null)); - assertThat(called.get()).isTrue(); - assertThatIllegalStateException().isThrownBy(() -> - template.setReturnCallback(mock(RabbitTemplate.ReturnCallback.class))); - RabbitTemplate template2 = new RabbitTemplate(); - org.springframework.amqp.rabbit.core.RabbitTemplate.ReturnCallback callback = - mock(org.springframework.amqp.rabbit.core.RabbitTemplate.ReturnCallback.class); - template2.setReturnCallback(callback); - template2.setReturnCallback(callback); - } - @Test void resourcesClearedAfterTxFails() throws IOException, TimeoutException { ConnectionFactory mockConnectionFactory = mock(ConnectionFactory.class); From 02f03863b3e3757368d3ef03ffff9d5e4a6ed841 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 21 Jun 2022 08:25:38 -0400 Subject: [PATCH 145/737] GH-1465: Part II - Super Stream SAC Resolves https://github.com/spring-projects/spring-amqp/issues/1465 Add support for single active consumers on super streams. Stop containers in test. Use Snapshot Repo Use snapshot repo; use TestContainers. --- build.gradle | 4 +- .../listener/StreamListenerContainer.java | 13 ++ .../stream/listener/SuperStreamSACTests.java | 142 ++++++++++++++++++ src/reference/asciidoc/quick-tour.adoc | 2 + src/reference/asciidoc/stream.adoc | 19 ++- src/reference/asciidoc/whats-new.adoc | 5 +- 6 files changed, 181 insertions(+), 4 deletions(-) create mode 100644 spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java diff --git a/build.gradle b/build.gradle index a74a9fe319..91ede6bd2d 100644 --- a/build.gradle +++ b/build.gradle @@ -58,7 +58,7 @@ ext { micrometerVersion = '1.10.0-SNAPSHOT' micrometerTracingVersion = '1.0.0-SNAPSHOT' mockitoVersion = '4.5.1' - rabbitmqStreamVersion = '0.4.0' + rabbitmqStreamVersion = '0.7.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.13.1' rabbitmqHttpClientVersion = '3.12.1' reactorVersion = '2020.0.18' @@ -105,7 +105,7 @@ allprojects { maven { url 'https://repo.spring.io/libs-milestone' } if (version.endsWith('-SNAPSHOT')) { maven { url 'https://repo.spring.io/libs-snapshot' } - maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } + // maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } } // maven { url 'https://repo.spring.io/libs-staging-local' } } diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java index 2a9684ccde..64cd4ccdcf 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java @@ -96,6 +96,19 @@ public void setQueueNames(String... queueNames) { this.builder.stream(queueNames[0]); } + /** + * Enable Single Active Consumer on a Super Stream. + * @param superStream the stream. + * @param name the consumer name. + * @since 3.0 + */ + public void superStream(String superStream, String name) { + Assert.notNull(superStream, "'superStream' cannot be null"); + this.builder.superStream(superStream) + .singleActiveConsumer() + .name(name); + } + /** * Get a {@link StreamMessageConverter} used to convert a * {@link com.rabbitmq.stream.Message} to a diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java new file mode 100644 index 0000000000..e9d3a28ced --- /dev/null +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java @@ -0,0 +1,142 @@ +/* + * 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.rabbit.stream.listener; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.core.Declarables; +import org.springframework.amqp.core.DirectExchange; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; +import org.springframework.rabbit.stream.config.SuperStream; +import org.springframework.rabbit.stream.support.AbstractIntegrationTests; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import com.rabbitmq.stream.Address; +import com.rabbitmq.stream.Environment; +import com.rabbitmq.stream.OffsetSpecification; + +/** + * @author Gary Russell + * @since 3.0 + * + */ +@SpringJUnitConfig +public class SuperStreamSACTests extends AbstractIntegrationTests { + + @Test + void superStream(@Autowired ApplicationContext context, @Autowired RabbitTemplate template, + @Autowired Environment env, @Autowired Config config, @Autowired RabbitAdmin admin, + @Autowired Declarables declarables) throws InterruptedException { + + template.getConnectionFactory().createConnection(); + StreamListenerContainer container1 = context.getBean(StreamListenerContainer.class, env, "one"); + container1.start(); + StreamListenerContainer container2 = context.getBean(StreamListenerContainer.class, env, "two"); + container2.start(); + StreamListenerContainer container3 = context.getBean(StreamListenerContainer.class, env, "three"); + container3.start(); + template.convertAndSend("ss.sac.test", "0", "foo"); + template.convertAndSend("ss.sac.test", "1", "bar"); + template.convertAndSend("ss.sac.test", "2", "baz"); + assertThat(config.latch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(config.messages.keySet()).contains("one", "two", "three"); + assertThat(config.info).contains("one:foo", "two:bar", "three:baz"); + container1.stop(); + container2.stop(); + container3.stop(); + clean(admin, declarables); + } + + private void clean(RabbitAdmin admin, Declarables declarables) { + declarables.getDeclarablesByType(Queue.class).forEach(queue -> admin.deleteQueue(queue.getName())); + declarables.getDeclarablesByType(DirectExchange.class).forEach(ex -> admin.deleteExchange(ex.getName())); + } + + @Configuration + public static class Config { + + final List info = new ArrayList<>(); + + final Map messages = new ConcurrentHashMap<>(); + + final CountDownLatch latch = new CountDownLatch(3); + + @Bean + CachingConnectionFactory cf() { + return new CachingConnectionFactory("localhost", amqpPort()); + } + + @Bean + RabbitAdmin admin(ConnectionFactory cf) { + return new RabbitAdmin(cf); + } + + @Bean + RabbitTemplate template(ConnectionFactory cf) { + return new RabbitTemplate(cf); + } + + @Bean + SuperStream superStream() { + return new SuperStream("ss.sac.test", 3); + } + + @Bean + static Environment environment() { + return Environment.builder() + .addressResolver(add -> new Address("localhost", streamPort())) + .build(); + } + + @Bean + @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) + StreamListenerContainer container(Environment env, String name) { + StreamListenerContainer container = new StreamListenerContainer(env); + container.superStream("ss.sac.test", "test"); + container.setupMessageListener(msg -> { + this.messages.put(name, msg); + this.info.add(name + ":" + new String(msg.getBody())); + this.latch.countDown(); + }); + container.setConsumerCustomizer((id, builder) -> builder.offset(OffsetSpecification.last())); + container.setAutoStartup(false); + return container; + } + + } + +} diff --git a/src/reference/asciidoc/quick-tour.adoc b/src/reference/asciidoc/quick-tour.adoc index af2aa94e2a..28a4a6bc4a 100644 --- a/src/reference/asciidoc/quick-tour.adoc +++ b/src/reference/asciidoc/quick-tour.adoc @@ -36,6 +36,8 @@ The minimum Spring Framework version dependency is 5.2.0. The minimum `amqp-client` Java client library version is 5.7.0. +The minimum `stream-client` Java client library for stream queues is 0.7.0. + ===== Very, Very Quick This section offers the fastest introduction. diff --git a/src/reference/asciidoc/stream.adoc b/src/reference/asciidoc/stream.adoc index bc18e09164..776eb5f3a7 100644 --- a/src/reference/asciidoc/stream.adoc +++ b/src/reference/asciidoc/stream.adoc @@ -182,4 +182,21 @@ The `RabbitAdmin` detects this bean and will declare the exchange (`my.super.str ===== Consuming Super Streams with Single Active Consumers -TBD. +Invoke the `superStream` method on the listener container to enable a single active consumer on a super stream. + +==== +[source, java] +---- +@Bean +@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) +StreamListenerContainer container(Environment env, String name) { + StreamListenerContainer container = new StreamListenerContainer(env); + container.superStream("ss.sac", "myConsumer"); + container.setupMessageListener(msg -> { + ... + }); + container.setConsumerCustomizer((id, builder) -> builder.offset(OffsetSpecification.last())); + return container; +} +---- +==== diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index 6825e4b558..2b43baca63 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -20,6 +20,9 @@ See <> for more information. ==== Stream Support Changes `RabbitStreamOperations2` and `RabbitStreamTemplate2` have been deprecated in favor of `RabbitStreamOperations` and `RabbitStreamTemplate` respectively. + +Super streams and single active consumers thereon are now supported. + See <> for more information. ==== `@RabbitListener` Changes @@ -35,4 +38,4 @@ See <> for more information. ==== Connection Factory Changes The default `addressShuffleMode` in `AbstractConnectionFactory` is now `RANDOM`. This results in connecting to a random host when multiple addresses are provided. -See <> for more information. \ No newline at end of file +See <> for more information. From bc01253053e7755aff78b34956963cbe1ed680ee Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 9 Aug 2022 15:44:58 -0400 Subject: [PATCH 146/737] Super Stream is not consumed for some reason --- .../rabbit/stream/listener/SuperStreamSACTests.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java index e9d3a28ced..7e30cf97a8 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java @@ -25,6 +25,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Declarables; @@ -55,6 +56,7 @@ * */ @SpringJUnitConfig +@Disabled public class SuperStreamSACTests extends AbstractIntegrationTests { @Test From 3df0f35597c14cad2d84a9d95ce2eef4d84e91fe Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 8 Sep 2022 12:47:32 -0400 Subject: [PATCH 147/737] GH-1465: Re-enable `SuperStreamSACTests` Fixes https://github.com/spring-projects/spring-amqp/issues/1465 Turns out the `latest` image has been cached locally and not really actual We will do respective housekeeping on CI server --- .../rabbit/stream/listener/SuperStreamSACTests.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java index 7e30cf97a8..e9d3a28ced 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java @@ -25,7 +25,6 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Declarables; @@ -56,7 +55,6 @@ * */ @SpringJUnitConfig -@Disabled public class SuperStreamSACTests extends AbstractIntegrationTests { @Test From f073c5ff9bc6f3e9671b19a3cf4a9c2c652d1ca7 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 8 Sep 2022 13:14:09 -0400 Subject: [PATCH 148/737] Upgrade to Testcontainers `1.17.3` --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 91ede6bd2d..6d1d3c5466 100644 --- a/build.gradle +++ b/build.gradle @@ -440,7 +440,7 @@ project('spring-rabbit-stream') { testRuntimeOnly "org.xerial.snappy:snappy-java:$snappyVersion" testRuntimeOnly "org.lz4:lz4-java:$lz4Version" testRuntimeOnly "com.github.luben:zstd-jni:$zstdJniVersion" - testImplementation "org.testcontainers:rabbitmq:1.15.3" + testImplementation "org.testcontainers:rabbitmq:1.17.3" testImplementation "org.apache.logging.log4j:log4j-slf4j-impl:$log4jVersion" } From 80f8c9c82c36c73f1528e65f1c5b14f0cb68999b Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 13 Sep 2022 10:29:32 -0400 Subject: [PATCH 149/737] GH-1473: Remove Unnecessary Deprecated Interfaces See https://github.com/spring-projects/spring-amqp/issues/1473 See https://github.com/spring-projects/spring-amqp/issues/1480 Migration interfaces to assist moving to `CompletableFuture` have been removed from 2.4.x due to Boot auto configuration issues. Therefore, these interfaces never existed in a released version of 2.4.x. --- .../amqp/core/AsyncAmqpTemplate2.java | 32 --------- .../producer/RabbitStreamOperations2.java | 34 ---------- .../producer/RabbitStreamTemplate2.java | 38 ----------- .../amqp/rabbit/AsyncRabbitTemplate2.java | 65 ------------------- .../rabbit/connection/CorrelationData.java | 11 ---- src/reference/asciidoc/amqp.adoc | 2 +- src/reference/asciidoc/appendix.adoc | 5 -- src/reference/asciidoc/stream.adoc | 10 +-- src/reference/asciidoc/whats-new.adoc | 5 +- 9 files changed, 8 insertions(+), 194 deletions(-) delete mode 100644 spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate2.java delete mode 100644 spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations2.java delete mode 100644 spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate2.java delete mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate2.java diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate2.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate2.java deleted file mode 100644 index cf835e7884..0000000000 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate2.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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.core; - -import java.util.concurrent.CompletableFuture; - -/** - * This interface was added in 2.4.7 to aid migration from methods returning - * {@code ListenableFuture}s to {@link CompletableFuture}s. - * - * @author Gary Russell - * @since 2.4.7 - * @deprecated in favor of {@link AsyncAmqpTemplate}. - */ -@Deprecated -public interface AsyncAmqpTemplate2 extends AsyncAmqpTemplate { - -} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations2.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations2.java deleted file mode 100644 index e1e9323748..0000000000 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations2.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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.rabbit.stream.producer; - -import java.util.concurrent.CompletableFuture; - -/** - * Provides methods for sending messages using a RabbitMQ Stream producer, - * returning {@link CompletableFuture}. - * This interface was added in 2.4.7 to aid migration from methods returning - * {@code ListenableFuture}s to {@link CompletableFuture}s. - * - * @author Gary Russell - * @since 2.4.7 - * @deprecated in favor of {@link RabbitStreamOperations}. - */ -@Deprecated -public interface RabbitStreamOperations2 extends RabbitStreamOperations { - -} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate2.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate2.java deleted file mode 100644 index 664515f404..0000000000 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate2.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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.rabbit.stream.producer; - -import java.util.concurrent.CompletableFuture; - -import com.rabbitmq.stream.Environment; - -/** - * This interface was added in 2.4.7 to aid migration from methods returning - * {@code ListenableFuture}s to {@link CompletableFuture}s. - * - * @author Gary Russell - * @since 2.8 - * @deprecated in favor of {@link RabbitStreamTemplate}. - */ -@Deprecated -public class RabbitStreamTemplate2 extends RabbitStreamTemplate implements RabbitStreamOperations2 { - - public RabbitStreamTemplate2(Environment environment, String streamName) { - super(environment, streamName); - } - -} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate2.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate2.java deleted file mode 100644 index 0bc828ec60..0000000000 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate2.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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; - -import java.util.concurrent.CompletableFuture; - -import org.springframework.amqp.core.AsyncAmqpTemplate2; -import org.springframework.amqp.rabbit.connection.ConnectionFactory; -import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; - -/** - * This class was added in 2.4.7 to aid migration from methods returning - * {@code ListenableFuture}s to {@link CompletableFuture}s. - * - * @author Gary Russell - * @since 2.4.7 - * @deprecated in favor of {@link AsyncRabbitTemplate}. - * - */ -@Deprecated -public class AsyncRabbitTemplate2 extends AsyncRabbitTemplate implements AsyncAmqpTemplate2 { - - public AsyncRabbitTemplate2(ConnectionFactory connectionFactory, String exchange, String routingKey, - String replyQueue, String replyAddress) { - super(connectionFactory, exchange, routingKey, replyQueue, replyAddress); - } - - public AsyncRabbitTemplate2(ConnectionFactory connectionFactory, String exchange, String routingKey, - String replyQueue) { - super(connectionFactory, exchange, routingKey, replyQueue); - } - - public AsyncRabbitTemplate2(ConnectionFactory connectionFactory, String exchange, String routingKey) { - super(connectionFactory, exchange, routingKey); - } - - public AsyncRabbitTemplate2(RabbitTemplate template, AbstractMessageListenerContainer container, - String replyAddress) { - super(template, container, replyAddress); - } - - public AsyncRabbitTemplate2(RabbitTemplate template, AbstractMessageListenerContainer container) { - super(template, container); - } - - public AsyncRabbitTemplate2(RabbitTemplate template) { - super(template); - } - -} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java index b6909d5180..9967ffa1f1 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java @@ -94,17 +94,6 @@ public CompletableFuture getFuture() { return this.future; } - /** - * Return a future to check the success/failure of the publish operation. - * @return the future. - * @since 2.4.7 - * @deprecated as of 3.0, in favor of {@link #getFuture()}. - */ - @Deprecated - public CompletableFuture getCompletableFuture() { - return this.future; - } - /** * Get the returned message and metadata, if any. Guaranteed to be populated before * the future is set. diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index dc011b1666..e62ec7a034 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -4677,7 +4677,7 @@ Version 2.0 introduced variants of these methods (`convertSendAndReceiveAsType`) You must configure the underlying `RabbitTemplate` with a `SmartMessageConverter`. See <> for more information. -The `AsyncRabbitTemplate2` (added to assist with migration to this release) is now deprecated in favor of `AsyncRabbitTemplate` which now returns `CompletableFuture` s instead of `ListenableFuture` s. +IMPORTANT: Starting with version 3.0, the `AsyncRabbitTemplate` methods now return `CompletableFuture` s instead of `ListenableFuture` s. [[remoting]] ===== Spring Remoting with AMQP diff --git a/src/reference/asciidoc/appendix.adoc b/src/reference/asciidoc/appendix.adoc index a48fb8d433..b78fc1da3e 100644 --- a/src/reference/asciidoc/appendix.adoc +++ b/src/reference/asciidoc/appendix.adoc @@ -34,11 +34,6 @@ Support remoting using Spring Framework’s RMI support is deprecated and will b The `Jackson2JsonMessageConverter` can now determine the charset from the `contentEncoding` header. See <> for more information. -==== AsyncRabbitTemplate - -The `AsyncRabbitTemplate` is deprecated in favor of `AsyncRabbitTemplate2` which returns `CompletableFuture` s instead of `ListenableFuture` s. -See <> for more information. - ==== Message Converter Changes The `Jackson2JsonMessageConverter` can now determine the charset from the `contentEncoding` header. diff --git a/src/reference/asciidoc/stream.adoc b/src/reference/asciidoc/stream.adoc index 776eb5f3a7..8de6ca0596 100644 --- a/src/reference/asciidoc/stream.adoc +++ b/src/reference/asciidoc/stream.adoc @@ -16,13 +16,13 @@ The `RabbitStreamTemplate` provides a subset of the `RabbitTemplate` (AMQP) func ---- public interface RabbitStreamOperations extends AutoCloseable { - ListenableFuture send(Message message); + ConvertableFuture send(Message message); - ListenableFuture convertAndSend(Object message); + ConvertableFuture convertAndSend(Object message); - ListenableFuture convertAndSend(Object message, @Nullable MessagePostProcessor mpp); + ConvertableFuture convertAndSend(Object message, @Nullable MessagePostProcessor mpp); - ListenableFuture send(com.rabbitmq.stream.Message message); + ConvertableFuture send(com.rabbitmq.stream.Message message); MessageBuilder messageBuilder(); @@ -67,7 +67,7 @@ The `ProducerCustomizer` provides a mechanism to customize the producer before i Refer to the https://rabbitmq.github.io/rabbitmq-stream-java-client/stable/htmlsingle/[Java Client Documentation] about customizing the `Environment` and `Producer`. -IMPORTANT: In version 2.4.7 `RabbitStreamOperations2` and `RabbitStreamTemplate2` were added to assist migration to this version; `RabbitStreamOperations2` and `RabbitStreamTemplate2` are now deprecated in favor of `RabbitStreamOperations` and `RabbitStreamTemplate` respectively. +IMPORTANT: Starting with version 3.0, the method return types are `CompletableFuture` instead of `ListenableFuture`. ==== Receiving Messages diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index 2b43baca63..995271622b 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -13,13 +13,12 @@ The remoting feature (using RMI) is no longer supported. ==== AsyncRabbitTemplate -The `AsyncRabbitTemplate2`, which was added in 2.4.7 to aid migration to this release, is deprecated in favor of `AsyncRabbitTemplate`. -The `AsyncRabbitTemplate` now returns `CompletableFuture` s instead of `ListenableFuture` s. +IMPORTANT: The `AsyncRabbitTemplate` now returns `CompletableFuture` s instead of `ListenableFuture` s. See <> for more information. ==== Stream Support Changes -`RabbitStreamOperations2` and `RabbitStreamTemplate2` have been deprecated in favor of `RabbitStreamOperations` and `RabbitStreamTemplate` respectively. +IMPORTANT: `RabbitStreamOperations` and `RabbitStreamTemplate` methods now return `CompletableFuture` instead of `ListenableFuture`. Super streams and single active consumers thereon are now supported. From 66e5c11b04cf9fa0d766e30fe3a6c4917ce54cca Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 19 Sep 2022 13:57:15 -0400 Subject: [PATCH 150/737] GH-1444: Listener Observability Initial Commit (#1500) * GH-1444: Listener Observability Initial Commit - expand scope to include error handler: https://github.com/spring-projects/spring-amqp/pull/1287 - tracing can't be used with a batch listener (multiple messages in listener call) * Rename Sender/Receiver contexts and other PR review comments. * Rename contexts to Rabbit...; supply default KeyValues via the conventions. * Javadoc polishing. * Don't add default KV to high-card KVs. * Fix previous commit. * Fix contextual name (receiver side). * Fix checkstyle. * Polish previous commit. * Fix contextual name (sender side) * Remove contextual names from observations. * Fix checkstyle. * Remove customization of KeyValues from conventions. * Add `getDefaultConvention()` to observations. * Fix since 3.0. * Support wider convention customization. * Convention type safety. * Fix Test - not sure why PR build succeeded. * Add Meters to ObservationTests. * Fix checkstyle. * Make INSTANCE final. * Add integration test. * Test all available integrations. * Remove redundant test code. * Move getContextualName to conventions. * Add docs. * Fix doc link. Co-authored-by: Artem Bilan * Remove unnecessary method overrides; make tag names more meaningful. * Move getName() from contexts to conventions. * Fix Race in Test * Fix Race in Test. Co-authored-by: Artem Bilan --- build.gradle | 2 + .../rabbit/connection/RabbitAccessor.java | 20 +- .../amqp/rabbit/core/RabbitTemplate.java | 78 ++++- .../AbstractMessageListenerContainer.java | 55 +++- ...ltRabbitListenerObservationConvention.java | 47 +++ ...ltRabbitTemplateObservationConvention.java | 47 +++ .../micrometer/RabbitListenerObservation.java | 75 +++++ .../RabbitListenerObservationConvention.java | 41 +++ .../RabbitMessageReceiverContext.java | 55 ++++ .../RabbitMessageSenderContext.java | 55 ++++ .../micrometer/RabbitTemplateObservation.java | 74 +++++ .../RabbitTemplateObservationConvention.java | 41 +++ .../support/micrometer/package-info.java | 6 + .../ObservationIntegrationTests.java | 155 ++++++++++ .../support/micrometer/ObservationTests.java | 280 ++++++++++++++++++ src/reference/asciidoc/amqp.adoc | 17 ++ src/reference/asciidoc/whats-new.adoc | 5 + 17 files changed, 1043 insertions(+), 10 deletions(-) create mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/DefaultRabbitListenerObservationConvention.java create mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/DefaultRabbitTemplateObservationConvention.java create mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java create mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservationConvention.java create mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java create mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageSenderContext.java create mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservation.java create mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservationConvention.java create mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/package-info.java create mode 100644 spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java create mode 100644 spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java diff --git a/build.gradle b/build.gradle index 6d1d3c5466..bb702cfe7a 100644 --- a/build.gradle +++ b/build.gradle @@ -387,6 +387,7 @@ project('spring-rabbit') { optionalApi "ch.qos.logback:logback-classic:$logbackVersion" optionalApi 'org.apache.logging.log4j:log4j-core' optionalApi 'io.micrometer:micrometer-core' + api 'io.micrometer:micrometer-observation' optionalApi 'io.micrometer:micrometer-tracing' // Spring Data projection message binding support optionalApi ("org.springframework.data:spring-data-commons") { @@ -398,6 +399,7 @@ project('spring-rabbit') { testApi project(':spring-rabbit-junit') testImplementation("com.willowtreeapps.assertk:assertk-jvm:$assertkVersion") testImplementation "org.hibernate.validator:hibernate-validator:$hibernateValidationVersion" + testImplementation 'io.micrometer:micrometer-observation-test' testImplementation 'io.micrometer:micrometer-tracing-bridge-brave' testImplementation 'io.micrometer:micrometer-tracing-test' testImplementation 'io.micrometer:micrometer-tracing-integration-test' diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java index afebee61bc..54c23bf687 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -21,10 +21,13 @@ import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.context.ApplicationContext; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.rabbitmq.client.Channel; +import io.micrometer.observation.ObservationRegistry; /** * @author Mark Fisher @@ -40,6 +43,8 @@ public abstract class RabbitAccessor implements InitializingBean { private volatile boolean transactional; + private ObservationRegistry observationRegistry; + public boolean isChannelTransacted() { return this.transactional; } @@ -113,4 +118,17 @@ protected RuntimeException convertRabbitAccessException(Exception ex) { return RabbitExceptionTranslator.convertRabbitAccessException(ex); } + protected void obtainObservationRegistry(@Nullable ApplicationContext appContext) { + if (this.observationRegistry == null && appContext != null) { + ObjectProvider registry = + appContext.getBeanProvider(ObservationRegistry.class); + this.observationRegistry = registry.getIfUnique(); + } + } + + @Nullable + protected ObservationRegistry getObservationRegistry() { + return this.observationRegistry; + } + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java index aecc38de30..2bbd7b98c5 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java @@ -74,6 +74,10 @@ import org.springframework.amqp.rabbit.support.MessagePropertiesConverter; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.amqp.rabbit.support.ValueExpression; +import org.springframework.amqp.rabbit.support.micrometer.DefaultRabbitTemplateObservationConvention; +import org.springframework.amqp.rabbit.support.micrometer.RabbitMessageSenderContext; +import org.springframework.amqp.rabbit.support.micrometer.RabbitTemplateObservation; +import org.springframework.amqp.rabbit.support.micrometer.RabbitTemplateObservationConvention; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.amqp.support.converter.SimpleMessageConverter; import org.springframework.amqp.support.converter.SmartMessageConverter; @@ -83,6 +87,8 @@ import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.context.expression.MapAccessor; import org.springframework.core.ParameterizedTypeReference; @@ -108,6 +114,8 @@ import com.rabbitmq.client.Return; import com.rabbitmq.client.ShutdownListener; import com.rabbitmq.client.ShutdownSignalException; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; /** *

@@ -152,7 +160,7 @@ * @since 1.0 */ public class RabbitTemplate extends RabbitAccessor // NOSONAR type line count - implements BeanFactoryAware, RabbitOperations, ChannelAwareMessageListener, + implements BeanFactoryAware, RabbitOperations, ChannelAwareMessageListener, ApplicationContextAware, ListenerContainerAware, PublisherCallbackChannel.Listener, BeanNameAware, DisposableBean { private static final String UNCHECKED = "unchecked"; @@ -198,6 +206,8 @@ public class RabbitTemplate extends RabbitAccessor // NOSONAR type line count private final AtomicInteger containerInstance = new AtomicInteger(); + private ApplicationContext applicationContext; + private String exchange = DEFAULT_EXCHANGE; private String routingKey = DEFAULT_ROUTING_KEY; @@ -258,13 +268,20 @@ public class RabbitTemplate extends RabbitAccessor // NOSONAR type line count private ErrorHandler replyErrorHandler; + private boolean useChannelForCorrelation; + + private boolean observationEnabled; + + @Nullable + private RabbitTemplateObservationConvention observationConvention; + private volatile boolean usingFastReplyTo; private volatile boolean evaluatedFastReplyTo; private volatile boolean isListener; - private boolean useChannelForCorrelation; + private volatile boolean observationRegistryObtained; /** * Convenient constructor for use with setter injection. Don't forget to set the connection factory. @@ -297,6 +314,29 @@ public final void setConnectionFactory(ConnectionFactory connectionFactory) { } } + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + /** + * Enable observation via micrometer. + * @param observationEnabled true to enable. + * @since 3.0 + */ + public void setObservationEnabled(boolean observationEnabled) { + this.observationEnabled = observationEnabled; + } + + /** + * Set an observation convention; used to add additional key/values to observations. + * @param observationConvention the convention. + * @since 3.0 + */ + public void setObservationConvention(RabbitTemplateObservationConvention observationConvention) { + this.observationConvention = observationConvention; + } + /** * The name of the default exchange to use for send operations when none is specified. Defaults to "" * which is the default exchange in the broker (per the AMQP specification). @@ -2348,7 +2388,7 @@ private boolean isPublisherConfirmsOrReturns(ConnectionFactory connectionFactory * @throws IOException If thrown by RabbitMQ API methods. */ public void doSend(Channel channel, String exchangeArg, String routingKeyArg, Message message, - boolean mandatory, @Nullable CorrelationData correlationData) throws IOException { + boolean mandatory, @Nullable CorrelationData correlationData) { String exch = nullSafeExchange(exchangeArg); String rKey = nullSafeRoutingKey(routingKeyArg); @@ -2378,7 +2418,7 @@ public void doSend(Channel channel, String exchangeArg, String routingKeyArg, Me logger.debug("Publishing message [" + messageToUse + "] on exchange [" + exch + "], routingKey = [" + rKey + "]"); } - sendToRabbit(channel, exch, rKey, mandatory, messageToUse); + observeTheSend(channel, messageToUse, mandatory, exch, rKey); // Check if commit needed if (isChannelLocallyTransacted(channel)) { // Transacted channel created by this template -> commit. @@ -2386,6 +2426,26 @@ public void doSend(Channel channel, String exchangeArg, String routingKeyArg, Me } } + protected void observeTheSend(Channel channel, Message message, boolean mandatory, String exch, String rKey) { + + if (!this.observationRegistryObtained) { + obtainObservationRegistry(this.applicationContext); + this.observationRegistryObtained = true; + } + Observation observation; + ObservationRegistry registry = getObservationRegistry(); + if (!this.observationEnabled || registry == null) { + observation = Observation.NOOP; + } + else { + observation = RabbitTemplateObservation.TEMPLATE_OBSERVATION.observation(this.observationConvention, + DefaultRabbitTemplateObservationConvention.INSTANCE, + new RabbitMessageSenderContext(message, this.beanName, exch + "/" + rKey), registry); + + } + observation.observe(() -> sendToRabbit(channel, exch, rKey, mandatory, message)); + } + /** * Return the exchange or the default exchange if null. * @param exchange the exchange. @@ -2407,10 +2467,16 @@ public String nullSafeRoutingKey(String rk) { } protected void sendToRabbit(Channel channel, String exchange, String routingKey, boolean mandatory, - Message message) throws IOException { + Message message) { + BasicProperties convertedMessageProperties = this.messagePropertiesConverter .fromMessageProperties(message.getMessageProperties(), this.encoding); - channel.basicPublish(exchange, routingKey, mandatory, convertedMessageProperties, message.getBody()); + try { + channel.basicPublish(exchange, routingKey, mandatory, convertedMessageProperties, message.getBody()); + } + catch (IOException ex) { + throw RabbitExceptionTranslator.convertRabbitAccessException(ex); + } } private void setupConfirm(Channel channel, Message message, @Nullable CorrelationData correlationDataArg) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index 4e276dd05c..e96633e56c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -63,6 +63,10 @@ import org.springframework.amqp.rabbit.support.DefaultMessagePropertiesConverter; import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; import org.springframework.amqp.rabbit.support.MessagePropertiesConverter; +import org.springframework.amqp.rabbit.support.micrometer.DefaultRabbitListenerObservationConvention; +import org.springframework.amqp.rabbit.support.micrometer.RabbitListenerObservation; +import org.springframework.amqp.rabbit.support.micrometer.RabbitListenerObservationConvention; +import org.springframework.amqp.rabbit.support.micrometer.RabbitMessageReceiverContext; import org.springframework.amqp.support.ConditionalExceptionLogger; import org.springframework.amqp.support.ConsumerTagStrategy; import org.springframework.amqp.support.postprocessor.MessagePostProcessorUtils; @@ -91,6 +95,8 @@ import com.rabbitmq.client.Channel; import com.rabbitmq.client.ShutdownSignalException; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; /** * @author Mark Pollack @@ -240,6 +246,8 @@ public abstract class AbstractMessageListenerContainer extends RabbitAccessor private boolean micrometerEnabled = true; + private boolean observationEnabled = false; + private boolean isBatchListener; private long consumeDelay; @@ -254,6 +262,9 @@ public abstract class AbstractMessageListenerContainer extends RabbitAccessor private MessageAckListener messageAckListener = (success, deliveryTag, cause) -> { }; + @Nullable + private RabbitListenerObservationConvention observationConvention; + @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; @@ -1151,14 +1162,36 @@ public void setMicrometerTags(Map tags) { } /** - * Set to false to disable micrometer listener timers. + * Set to false to disable micrometer listener timers. When true, ignored + * if {@link #setObservationEnabled(boolean)} is set to true. * @param micrometerEnabled false to disable. * @since 2.2 + * @see #setObservationEnabled(boolean) */ public void setMicrometerEnabled(boolean micrometerEnabled) { this.micrometerEnabled = micrometerEnabled; } + /** + * Enable observation via micrometer; disables basic Micrometer timers enabled + * by {@link #setMicrometerEnabled(boolean)}. + * @param observationEnabled true to enable. + * @since 3.0 + * @see #setMicrometerEnabled(boolean) + */ + public void setObservationEnabled(boolean observationEnabled) { + this.observationEnabled = observationEnabled; + } + + /** + * Set an observation convention; used to add additional key/values to observations. + * @param observationConvention the convention. + * @since 3.0 + */ + public void setObservationConvention(RabbitListenerObservationConvention observationConvention) { + this.observationConvention = observationConvention; + } + /** * Get the consumeDelay - a time to wait before consuming in ms. * @return the consume delay. @@ -1230,7 +1263,7 @@ public void afterPropertiesSet() { validateConfiguration(); initialize(); try { - if (this.micrometerHolder == null && MICROMETER_PRESENT && this.micrometerEnabled + if (this.micrometerHolder == null && MICROMETER_PRESENT && this.micrometerEnabled && !this.observationEnabled && this.applicationContext != null) { String id = getListenerId(); if (id == null) { @@ -1402,6 +1435,7 @@ public void start() { } } } + obtainObservationRegistry(this.applicationContext); try { logger.debug("Starting Rabbit listener container."); configureAdminIfNeeded(); @@ -1499,8 +1533,23 @@ protected void invokeErrorHandler(Throwable ex) { * @see #invokeListener * @see #handleListenerException */ - @SuppressWarnings(UNCHECKED) protected void executeListener(Channel channel, Object data) { + Observation observation; + ObservationRegistry registry = getObservationRegistry(); + if (!this.observationEnabled || data instanceof List || registry == null) { + observation = Observation.NOOP; + } + else { + Message message = (Message) data; + observation = RabbitListenerObservation.LISTENER_OBSERVATION.observation(this.observationConvention, + DefaultRabbitListenerObservationConvention.INSTANCE, + new RabbitMessageReceiverContext(message, getListenerId()), registry); + } + observation.observe(() -> executeListenerAndHandleException(channel, data)); + } + + @SuppressWarnings(UNCHECKED) + protected void executeListenerAndHandleException(Channel channel, Object data) { if (!isRunning()) { if (logger.isWarnEnabled()) { logger.warn( diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/DefaultRabbitListenerObservationConvention.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/DefaultRabbitListenerObservationConvention.java new file mode 100644 index 0000000000..ae6f4488b9 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/DefaultRabbitListenerObservationConvention.java @@ -0,0 +1,47 @@ +/* + * 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.support.micrometer; + +import io.micrometer.common.KeyValues; + +/** + * Default {@link RabbitListenerObservationConvention} for Rabbit listener key values. + * + * @author Gary Russell + * @since 3.0 + * + */ +public class DefaultRabbitListenerObservationConvention implements RabbitListenerObservationConvention { + + /** + * A singleton instance of the convention. + */ + public static final DefaultRabbitListenerObservationConvention INSTANCE = + new DefaultRabbitListenerObservationConvention(); + + @Override + public KeyValues getLowCardinalityKeyValues(RabbitMessageReceiverContext context) { + return KeyValues.of(RabbitListenerObservation.ListenerLowCardinalityTags.LISTENER_ID.asString(), + context.getListenerId()); + } + + @Override + public String getContextualName(RabbitMessageReceiverContext context) { + return context.getSource() + " receive"; + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/DefaultRabbitTemplateObservationConvention.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/DefaultRabbitTemplateObservationConvention.java new file mode 100644 index 0000000000..285d52b835 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/DefaultRabbitTemplateObservationConvention.java @@ -0,0 +1,47 @@ +/* + * 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.support.micrometer; + +import io.micrometer.common.KeyValues; + +/** + * Default {@link RabbitTemplateObservationConvention} for Rabbit template key values. + * + * @author Gary Russell + * @since 3.0 + * + */ +public class DefaultRabbitTemplateObservationConvention implements RabbitTemplateObservationConvention { + + /** + * A singleton instance of the convention. + */ + public static final DefaultRabbitTemplateObservationConvention INSTANCE = + new DefaultRabbitTemplateObservationConvention(); + + @Override + public KeyValues getLowCardinalityKeyValues(RabbitMessageSenderContext context) { + return KeyValues.of(RabbitTemplateObservation.TemplateLowCardinalityTags.BEAN_NAME.asString(), + context.getBeanName()); + } + + @Override + public String getContextualName(RabbitMessageSenderContext context) { + return context.getDestination() + " send"; + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java new file mode 100644 index 0000000000..8454a987a3 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java @@ -0,0 +1,75 @@ +/* + * 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.support.micrometer; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.docs.DocumentedObservation; + +/** + * Spring Rabbit Observation for listeners. + * + * @author Gary Russell + * @since 3.0 + * + */ +public enum RabbitListenerObservation implements DocumentedObservation { + + /** + * Observation for Rabbit listeners. + */ + LISTENER_OBSERVATION { + + + @Override + public Class> getDefaultConvention() { + return DefaultRabbitListenerObservationConvention.class; + } + + @Override + public String getPrefix() { + return "spring.rabbit.listener"; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return ListenerLowCardinalityTags.values(); + } + + }; + + /** + * Low cardinality tags. + */ + public enum ListenerLowCardinalityTags implements KeyName { + + /** + * Listener id. + */ + LISTENER_ID { + + @Override + public String asString() { + return "spring.rabbit.listener.id"; + } + + } + + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservationConvention.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservationConvention.java new file mode 100644 index 0000000000..bbf1f27df5 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservationConvention.java @@ -0,0 +1,41 @@ +/* + * 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.support.micrometer; + +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationConvention; + +/** + * {@link ObservationConvention} for Rabbit listener key values. + * + * @author Gary Russell + * @since 3.0 + * + */ +public interface RabbitListenerObservationConvention extends ObservationConvention { + + @Override + default boolean supportsContext(Context context) { + return context instanceof RabbitMessageReceiverContext; + } + + @Override + default String getName() { + return "spring.rabbit.listener"; + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java new file mode 100644 index 0000000000..07ffebd732 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java @@ -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.support.micrometer; + +import org.springframework.amqp.core.Message; + +import io.micrometer.observation.transport.ReceiverContext; + +/** + * {@link ReceiverContext} for {@link Message}s. + * + * @author Gary Russell + * @since 3.0 + * + */ +public class RabbitMessageReceiverContext extends ReceiverContext { + + private final String listenerId; + + private final Message message; + + public RabbitMessageReceiverContext(Message message, String listenerId) { + super((carrier, key) -> carrier.getMessageProperties().getHeader(key)); + setCarrier(message); + this.message = message; + this.listenerId = listenerId; + } + + public String getListenerId() { + return this.listenerId; + } + + /** + * Return the source (queue) for this message. + * @return the source. + */ + public String getSource() { + return this.message.getMessageProperties().getConsumerQueue(); + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageSenderContext.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageSenderContext.java new file mode 100644 index 0000000000..b1b25755d4 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageSenderContext.java @@ -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.support.micrometer; + +import org.springframework.amqp.core.Message; + +import io.micrometer.observation.transport.SenderContext; + +/** + * {@link SenderContext} for {@link Message}s. + * + * @author Gary Russell + * @since 3.0 + * + */ +public class RabbitMessageSenderContext extends SenderContext { + + private final String beanName; + + private final String destination; + + public RabbitMessageSenderContext(Message message, String beanName, String destination) { + super((carrier, key, value) -> message.getMessageProperties().setHeader(key, value)); + setCarrier(message); + this.beanName = beanName; + this.destination = destination; + } + + public String getBeanName() { + return this.beanName; + } + + /** + * Return the destination - {@code exchange/routingKey}. + * @return the destination. + */ + public String getDestination() { + return this.destination; + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservation.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservation.java new file mode 100644 index 0000000000..01fb63f6c2 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservation.java @@ -0,0 +1,74 @@ +/* + * 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.support.micrometer; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.docs.DocumentedObservation; + +/** + * Spring RabbitMQ Observation for {@link org.springframework.amqp.rabbit.core.RabbitTemplate}. + * + * @author Gary Russell + * @since 3.0 + * + */ +public enum RabbitTemplateObservation implements DocumentedObservation { + + /** + * {@link org.springframework.kafka.core.KafkaTemplate} observation. + */ + TEMPLATE_OBSERVATION { + + @Override + public Class> getDefaultConvention() { + return DefaultRabbitTemplateObservationConvention.class; + } + + @Override + public String getPrefix() { + return "spring.rabbit.template"; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return TemplateLowCardinalityTags.values(); + } + + }; + + /** + * Low cardinality tags. + */ + public enum TemplateLowCardinalityTags implements KeyName { + + /** + * Bean name of the template. + */ + BEAN_NAME { + + @Override + public String asString() { + return "spring.rabbit.template.name"; + } + + } + + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservationConvention.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservationConvention.java new file mode 100644 index 0000000000..2128d3dd9b --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservationConvention.java @@ -0,0 +1,41 @@ +/* + * 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.support.micrometer; + +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationConvention; + +/** + * {@link ObservationConvention} for Rabbit template key values. + * + * @author Gary Russell + * @since 3.0 + * + */ +public interface RabbitTemplateObservationConvention extends ObservationConvention { + + @Override + default boolean supportsContext(Context context) { + return context instanceof RabbitMessageSenderContext; + } + + @Override + default String getName() { + return "spring.rabbit.template"; + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/package-info.java new file mode 100644 index 0000000000..8131427dac --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/package-info.java @@ -0,0 +1,6 @@ +/** + * Provides classes for Micrometer support. + */ +@org.springframework.lang.NonNullApi +@org.springframework.lang.NonNullFields +package org.springframework.amqp.rabbit.support.micrometer; diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java new file mode 100644 index 0000000000..2611785c8f --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java @@ -0,0 +1,155 @@ +/* + * 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.support.micrometer; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.springframework.amqp.rabbit.annotation.EnableRabbit; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.junit.RabbitAvailable; +import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.Message; + +import io.micrometer.common.KeyValues; +import io.micrometer.core.tck.MeterRegistryAssert; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.Span.Kind; +import io.micrometer.tracing.exporter.FinishedSpan; +import io.micrometer.tracing.test.SampleTestRunner; +import io.micrometer.tracing.test.simple.SpanAssert; +import io.micrometer.tracing.test.simple.SpansAssert; + +/** + * @author Artem Bilan + * @author Gary Russell + * + * @since 3.0 + */ +@RabbitAvailable(queues = { "int.observation.testQ1", "int.observation.testQ2" }) +public class ObservationIntegrationTests extends SampleTestRunner { + + @Override + public SampleTestRunnerConsumer yourCode() { + // template -> listener -> template -> listener + return (bb, meterRegistry) -> { + ObservationRegistry observationRegistry = getObservationRegistry(); + try (AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext()) { + applicationContext.registerBean(ObservationRegistry.class, () -> observationRegistry); + applicationContext.register(Config.class); + applicationContext.refresh(); + applicationContext.getBean(RabbitTemplate.class).convertAndSend("int.observation.testQ1", "test"); + assertThat(applicationContext.getBean(Listener.class).latch1.await(10, TimeUnit.SECONDS)).isTrue(); + } + + List finishedSpans = bb.getFinishedSpans(); + SpansAssert.assertThat(finishedSpans) + .haveSameTraceId() + .hasSize(4); + SpanAssert.assertThat(finishedSpans.get(0)) + .hasKindEqualTo(Kind.PRODUCER) + .hasTag("spring.rabbit.template.name", "template"); + SpanAssert.assertThat(finishedSpans.get(1)) + .hasKindEqualTo(Kind.PRODUCER) + .hasTag("spring.rabbit.template.name", "template"); + SpanAssert.assertThat(finishedSpans.get(2)) + .hasKindEqualTo(Kind.CONSUMER) + .hasTag("spring.rabbit.listener.id", "obs1"); + SpanAssert.assertThat(finishedSpans.get(3)) + .hasKindEqualTo(Kind.CONSUMER) + .hasTag("spring.rabbit.listener.id", "obs2"); + + MeterRegistryAssert.assertThat(getMeterRegistry()) + .hasTimerWithNameAndTags("spring.rabbit.template", + KeyValues.of("spring.rabbit.template.name", "template")) + .hasTimerWithNameAndTags("spring.rabbit.template", + KeyValues.of("spring.rabbit.template.name", "template")) + .hasTimerWithNameAndTags("spring.rabbit.listener", + KeyValues.of("spring.rabbit.listener.id", "obs1")) + .hasTimerWithNameAndTags("spring.rabbit.listener", + KeyValues.of("spring.rabbit.listener.id", "obs2")); + }; + } + + + @Configuration + @EnableRabbit + public static class Config { + + @Bean + CachingConnectionFactory ccf() { + return new CachingConnectionFactory(RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); + } + + @Bean + RabbitTemplate template(CachingConnectionFactory ccf) { + RabbitTemplate template = new RabbitTemplate(ccf); + template.setObservationEnabled(true); + return template; + } + + @Bean + SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(CachingConnectionFactory ccf) { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(ccf); + factory.setContainerCustomizer(container -> container.setObservationEnabled(true)); + return factory; + } + + @Bean + Listener listener(RabbitTemplate template) { + return new Listener(template); + } + + } + + public static class Listener { + + private final RabbitTemplate template; + + final CountDownLatch latch1 = new CountDownLatch(1); + + volatile Message message; + + public Listener(RabbitTemplate template) { + this.template = template; + } + + @RabbitListener(id = "obs1", queues = "int.observation.testQ1") + void listen1(Message in) { + this.template.convertAndSend("int.observation.testQ2", in); + } + + @RabbitListener(id = "obs2", queues = "int.observation.testQ2") + void listen2(Message in) { + this.message = in; + this.latch1.countDown(); + } + + } + + +} diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java new file mode 100644 index 0000000000..58e3d911b3 --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java @@ -0,0 +1,280 @@ +/* + * 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.support.micrometer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.util.Arrays; +import java.util.Deque; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.rabbit.annotation.EnableRabbit; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.junit.RabbitAvailable; +import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; +import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; +import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.Nullable; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import io.micrometer.common.KeyValues; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.core.tck.MeterRegistryAssert; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.tck.TestObservationRegistry; +import io.micrometer.tracing.Span; +import io.micrometer.tracing.TraceContext; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.handler.DefaultTracingObservationHandler; +import io.micrometer.tracing.handler.PropagatingReceiverTracingObservationHandler; +import io.micrometer.tracing.handler.PropagatingSenderTracingObservationHandler; +import io.micrometer.tracing.propagation.Propagator; +import io.micrometer.tracing.test.simple.SimpleSpan; +import io.micrometer.tracing.test.simple.SimpleTracer; + +/** + * @author Gary Russell + * @since 3.0 + * + */ +@SpringJUnitConfig +@RabbitAvailable(queues = { "observation.testQ1", "observation.testQ2" }) +public class ObservationTests { + + @Test + void endToEnd(@Autowired Listener listener, @Autowired RabbitTemplate template, + @Autowired SimpleTracer tracer, @Autowired RabbitListenerEndpointRegistry rler, + @Autowired MeterRegistry meterRegistry) + throws InterruptedException { + + template.convertAndSend("observation.testQ1", "test"); + assertThat(listener.latch1.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(listener.message) + .extracting(msg -> msg.getMessageProperties().getHeaders()) + .hasFieldOrPropertyWithValue("foo", "some foo value") + .hasFieldOrPropertyWithValue("bar", "some bar value"); + Deque spans = tracer.getSpans(); + assertThat(spans).hasSize(4); + SimpleSpan span = spans.poll(); + assertThat(span.getTags()).containsEntry("spring.rabbit.template.name", "template"); + assertThat(span.getName()).isEqualTo("/observation.testQ1 send"); + await().until(() -> spans.peekFirst().getTags().size() == 3); + span = spans.poll(); + assertThat(span.getTags()) + .containsAllEntriesOf( + Map.of("spring.rabbit.listener.id", "obs1", "foo", "some foo value", "bar", "some bar value")); + assertThat(span.getName()).isEqualTo("observation.testQ1 receive"); + await().until(() -> spans.peekFirst().getTags().size() == 1); + span = spans.poll(); + assertThat(span.getTags()).containsEntry("spring.rabbit.template.name", "template"); + assertThat(span.getName()).isEqualTo("/observation.testQ2 send"); + await().until(() -> spans.peekFirst().getTags().size() == 3); + span = spans.poll(); + assertThat(span.getTags()) + .containsAllEntriesOf( + Map.of("spring.rabbit.listener.id", "obs2", "foo", "some foo value", "bar", "some bar value")); + assertThat(span.getName()).isEqualTo("observation.testQ2 receive"); + template.setObservationConvention(new DefaultRabbitTemplateObservationConvention() { + + @Override + public KeyValues getLowCardinalityKeyValues(RabbitMessageSenderContext context) { + return super.getLowCardinalityKeyValues(context).and("foo", "bar"); + } + + }); + ((AbstractMessageListenerContainer) rler.getListenerContainer("obs1")).setObservationConvention( + new DefaultRabbitListenerObservationConvention() { + + @Override + public KeyValues getLowCardinalityKeyValues(RabbitMessageReceiverContext context) { + return super.getLowCardinalityKeyValues(context).and("baz", "qux"); + } + + }); + rler.getListenerContainer("obs1").stop(); + rler.getListenerContainer("obs1").start(); + template.convertAndSend("observation.testQ1", "test"); + assertThat(listener.latch2.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(listener.message) + .extracting(msg -> msg.getMessageProperties().getHeaders()) + .hasFieldOrPropertyWithValue("foo", "some foo value") + .hasFieldOrPropertyWithValue("bar", "some bar value"); + assertThat(spans).hasSize(4); + span = spans.poll(); + assertThat(span.getTags()).containsEntry("spring.rabbit.template.name", "template"); + assertThat(span.getTags()).containsEntry("foo", "bar"); + assertThat(span.getName()).isEqualTo("/observation.testQ1 send"); + await().until(() -> spans.peekFirst().getTags().size() == 4); + span = spans.poll(); + assertThat(span.getTags()) + .containsAllEntriesOf(Map.of("spring.rabbit.listener.id", "obs1", "foo", "some foo value", "bar", + "some bar value", "baz", "qux")); + assertThat(span.getName()).isEqualTo("observation.testQ1 receive"); + await().until(() -> spans.peekFirst().getTags().size() == 2); + span = spans.poll(); + assertThat(span.getTags()).containsEntry("spring.rabbit.template.name", "template"); + assertThat(span.getTags()).containsEntry("foo", "bar"); + assertThat(span.getName()).isEqualTo("/observation.testQ2 send"); + await().until(() -> spans.peekFirst().getTags().size() == 3); + span = spans.poll(); + assertThat(span.getTags()) + .containsAllEntriesOf( + Map.of("spring.rabbit.listener.id", "obs2", "foo", "some foo value", "bar", "some bar value")); + assertThat(span.getTags()).doesNotContainEntry("baz", "qux"); + assertThat(span.getName()).isEqualTo("observation.testQ2 receive"); + MeterRegistryAssert.assertThat(meterRegistry) + .hasTimerWithNameAndTags("spring.rabbit.template", + KeyValues.of("spring.rabbit.template.name", "template")) + .hasTimerWithNameAndTags("spring.rabbit.template", + KeyValues.of("spring.rabbit.template.name", "template", "foo", "bar")) + .hasTimerWithNameAndTags("spring.rabbit.listener", KeyValues.of("spring.rabbit.listener.id", "obs1")) + .hasTimerWithNameAndTags("spring.rabbit.listener", + KeyValues.of("spring.rabbit.listener.id", "obs1", "baz", "qux")) + .hasTimerWithNameAndTags("spring.rabbit.listener", KeyValues.of("spring.rabbit.listener.id", "obs2")); + } + + @Configuration + @EnableRabbit + public static class Config { + + @Bean + CachingConnectionFactory ccf() { + return new CachingConnectionFactory(RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); + } + + @Bean + RabbitTemplate template(CachingConnectionFactory ccf) { + RabbitTemplate template = new RabbitTemplate(ccf); + template.setObservationEnabled(true); + return template; + } + + @Bean + SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(CachingConnectionFactory ccf) { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(ccf); + factory.setContainerCustomizer(container -> container.setObservationEnabled(true)); + return factory; + } + + @Bean + SimpleTracer simpleTracer() { + return new SimpleTracer(); + } + + @Bean + MeterRegistry meterRegistry() { + return new SimpleMeterRegistry(); + } + + @Bean + ObservationRegistry observationRegistry(Tracer tracer, Propagator propagator, MeterRegistry meterRegistry) { + TestObservationRegistry observationRegistry = TestObservationRegistry.create(); + observationRegistry.observationConfig().observationHandler( + // Composite will pick the first matching handler + new ObservationHandler.FirstMatchingCompositeObservationHandler( + // This is responsible for creating a child span on the sender side + new PropagatingSenderTracingObservationHandler<>(tracer, propagator), + // This is responsible for creating a span on the receiver side + new PropagatingReceiverTracingObservationHandler<>(tracer, propagator), + // This is responsible for creating a default span + new DefaultTracingObservationHandler(tracer))) + .observationHandler(new DefaultMeterObservationHandler(meterRegistry)); + return observationRegistry; + } + + @Bean + Propagator propagator(Tracer tracer) { + return new Propagator() { + + // List of headers required for tracing propagation + @Override + public List fields() { + return Arrays.asList("foo", "bar"); + } + + // This is called on the producer side when the message is being sent + // Normally we would pass information from tracing context - for tests we don't need to + @Override + public void inject(TraceContext context, @Nullable C carrier, Setter setter) { + setter.set(carrier, "foo", "some foo value"); + setter.set(carrier, "bar", "some bar value"); + } + + // This is called on the consumer side when the message is consumed + // Normally we would use tools like Extractor from tracing but for tests we are just manually creating a span + @Override + public Span.Builder extract(C carrier, Getter getter) { + String foo = getter.get(carrier, "foo"); + String bar = getter.get(carrier, "bar"); + return tracer.spanBuilder().tag("foo", foo).tag("bar", bar); + } + }; + } + + @Bean + Listener listener(RabbitTemplate template) { + return new Listener(template); + } + + } + + public static class Listener { + + private final RabbitTemplate template; + + final CountDownLatch latch1 = new CountDownLatch(1); + + final CountDownLatch latch2 = new CountDownLatch(2); + + volatile Message message; + + public Listener(RabbitTemplate template) { + this.template = template; + } + + @RabbitListener(id = "obs1", queues = "observation.testQ1") + void listen1(Message in) { + this.template.send("observation.testQ2", in); + } + + @RabbitListener(id = "obs2", queues = "observation.testQ2") + void listen2(Message in) { + this.message = in; + this.latch1.countDown(); + this.latch2.countDown(); + } + + } + +} diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index e62ec7a034..9095eff965 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -3764,6 +3764,23 @@ The timers are named `spring.rabbitmq.listener` and have the following tags: You can add additional tags using the `micrometerTags` container property. +Also see <>. + +[[observation]] +===== Micrometer Observation + +Using Micrometer for observation is now supported, since version 3.0, for the `RabbitTemplate` and listener containers. + +Set `observationEnabled` on each component to enable observation; this will disable <> because the timers will now be managed with each observation. + +Refer to https://micrometer.io/docs/tracing[Micrometer Tracing] for more information. + +To add tags to timers/traces, configure a custom `RabbitTemplateObservationConvention` or `RabbitListenerObservationConvention` to the template or listener container, respectively. + +The default implementations add the `bean.name` tag for template observations and `listener.id` tag for containers. + +You can either subclass `DefaultRabbitTemplateObservationConvention` or `DefaultRabbitListenerObservationConvention` or provide completely new implementations. + [[containers-and-broker-named-queues]] ==== Containers and Broker-Named queues diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index 995271622b..d11a5e719d 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -11,6 +11,11 @@ This version requires Spring Framework 6.0 and Java 17 The remoting feature (using RMI) is no longer supported. +==== Observation + +Enabling observation for timers and tracing using Micrometer is now supported. +See <> for more information. + ==== AsyncRabbitTemplate IMPORTANT: The `AsyncRabbitTemplate` now returns `CompletableFuture` s instead of `ListenableFuture` s. From 57276fcf3a271cda25ad6c5906d61f4faf1dba7a Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 19 Sep 2022 14:29:55 -0400 Subject: [PATCH 151/737] GH-1444: Remove Unused Field from Test --- .../support/micrometer/ObservationIntegrationTests.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java index 2611785c8f..43f4de1136 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java @@ -22,6 +22,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.EnableRabbit; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; @@ -32,7 +33,6 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.messaging.Message; import io.micrometer.common.KeyValues; import io.micrometer.core.tck.MeterRegistryAssert; @@ -132,8 +132,6 @@ public static class Listener { final CountDownLatch latch1 = new CountDownLatch(1); - volatile Message message; - public Listener(RabbitTemplate template) { this.template = template; } @@ -145,7 +143,6 @@ void listen1(Message in) { @RabbitListener(id = "obs2", queues = "int.observation.testQ2") void listen2(Message in) { - this.message = in; this.latch1.countDown(); } From edca62512f5b3dd10893dd45f6c9aac28de4ef5b Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 19 Sep 2022 14:30:18 -0400 Subject: [PATCH 152/737] Upgrade Versions; Prepare for Milestone Release --- build.gradle | 24 +++++++++---------- .../RetryInterceptorBuilderSupportTests.java | 9 +++++-- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/build.gradle b/build.gradle index bb702cfe7a..011254552c 100644 --- a/build.gradle +++ b/build.gradle @@ -39,7 +39,7 @@ ext { modifiedFiles = files(grgit.status().unstaged.modified).filter{ f -> f.name.endsWith('.java') || f.name.endsWith('.kt') } - assertjVersion = '3.22.0' + assertjVersion = '3.23.1' assertkVersion = '0.24' awaitilityVersion = '4.2.0' commonsCompressVersion = '1.20' @@ -48,24 +48,24 @@ ext { googleJsr305Version = '3.0.2' hamcrestVersion = '2.2' hibernateValidationVersion = '7.0.4.Final' - jacksonBomVersion = '2.13.3' + jacksonBomVersion = '2.13.4' jaywayJsonPathVersion = '2.6.0' junit4Version = '4.13.2' - junitJupiterVersion = '5.8.2' - log4jVersion = '2.17.2' + junitJupiterVersion = '5.9.0' + log4jVersion = '2.18.0' logbackVersion = '1.2.3' lz4Version = '1.8.0' - micrometerVersion = '1.10.0-SNAPSHOT' - micrometerTracingVersion = '1.0.0-SNAPSHOT' - mockitoVersion = '4.5.1' + micrometerVersion = '1.10.0-M6' + micrometerTracingVersion = '1.0.0-M8' + mockitoVersion = '4.8.0' rabbitmqStreamVersion = '0.7.0' - rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.13.1' + rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.16.0' rabbitmqHttpClientVersion = '3.12.1' - reactorVersion = '2020.0.18' + reactorVersion = '2022.0.0-M6' snappyVersion = '1.1.8.4' - springDataVersion = '2022.0.0-M3' - springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.0-SNAPSHOT' - springRetryVersion = '1.3.3' + springDataVersion = '2022.0.0-M6' + springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.0-M6' + springRetryVersion = '2.0.0-M1' zstdJniVersion = '1.5.0-2' } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RetryInterceptorBuilderSupportTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RetryInterceptorBuilderSupportTests.java index bf7f3a7e38..49581837ea 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RetryInterceptorBuilderSupportTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RetryInterceptorBuilderSupportTests.java @@ -25,6 +25,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; import org.aopalliance.intercept.MethodInterceptor; import org.junit.jupiter.api.Test; @@ -103,7 +104,9 @@ public void testWithCustomBackOffPolicy() { .build(); assertThat(TestUtils.getPropertyValue(interceptor, "retryOperations.retryPolicy.maxAttempts")).isEqualTo(5); - assertThat(TestUtils.getPropertyValue(interceptor, "retryOperations.backOffPolicy.backOffPeriod")).isEqualTo(1000L); + assertThat(TestUtils + .getPropertyValue(interceptor, "retryOperations.backOffPolicy.backOffPeriod", Supplier.class).get()) + .isEqualTo(1000L); } @Test @@ -120,7 +123,9 @@ public void testWithCustomNewMessageIdentifier() throws Exception { .build(); assertThat(TestUtils.getPropertyValue(interceptor, "retryOperations.retryPolicy.maxAttempts")).isEqualTo(5); - assertThat(TestUtils.getPropertyValue(interceptor, "retryOperations.backOffPolicy.backOffPeriod")).isEqualTo(1000L); + assertThat(TestUtils + .getPropertyValue(interceptor, "retryOperations.backOffPolicy.backOffPeriod", Supplier.class).get()) + .isEqualTo(1000L); final AtomicInteger count = new AtomicInteger(); Foo delegate = createDelegate(interceptor, count); Message message = MessageBuilder.withBody("".getBytes()).setMessageId("foo").setRedelivered(false).build(); From 0da0f88ff5631e17b414db9d6706cd500f0000d6 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 19 Sep 2022 19:05:57 +0000 Subject: [PATCH 153/737] [artifactory-release] Release version 3.0.0-M4 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 2477c6a3c5..b0729143e1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.0-SNAPSHOT +version=3.0.0-M4 org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true From 322605d25c22549ba9a5f7d74f3eda67879c82f4 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 19 Sep 2022 19:05:59 +0000 Subject: [PATCH 154/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index b0729143e1..2477c6a3c5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.0-M4 +version=3.0.0-SNAPSHOT org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true From 094781ecceb581f4f802dce161c5d9ad4124823c Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 22 Sep 2022 15:31:56 -0400 Subject: [PATCH 155/737] GH-1465: Add Routing Key Strategy to SuperStream To support spring-cloud-stream. * Only include the ordinal in the queue name, not the whole routing key. --- .../rabbit/stream/config/SuperStream.java | 30 +++++++++++++++---- .../stream/listener/SuperStreamSACTests.java | 12 +++++--- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStream.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStream.java index 73fe76a9ff..0d74183d79 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStream.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStream.java @@ -20,6 +20,8 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.function.BiFunction; +import java.util.stream.Collectors; import java.util.stream.IntStream; import org.springframework.amqp.core.Binding; @@ -28,6 +30,7 @@ import org.springframework.amqp.core.Declarables; import org.springframework.amqp.core.DirectExchange; import org.springframework.amqp.core.Queue; +import org.springframework.util.Assert; /** * Create Super Stream Topology {@link Declarable}s. @@ -44,16 +47,33 @@ public class SuperStream extends Declarables { * @param partitions the number of partitions. */ public SuperStream(String name, int partitions) { - super(declarables(name, partitions)); + this(name, partitions, (q, i) -> IntStream.range(0, i) + .mapToObj(String::valueOf) + .collect(Collectors.toList())); } - private static Collection declarables(String name, int partitions) { + /** + * Create a Super Stream with the provided parameters. + * @param name the stream name. + * @param partitions the number of partitions. + * @param routingKeyStrategy a strategy to determine routing keys to use for the + * partitions. The first parameter is the queue name, the second the number of + * partitions, the returned list must have a size equal to the partitions. + */ + public SuperStream(String name, int partitions, BiFunction> routingKeyStrategy) { + super(declarables(name, partitions, routingKeyStrategy)); + } + + private static Collection declarables(String name, int partitions, + BiFunction> routingKeyStrategy) { + List declarables = new ArrayList<>(); - String[] rks = IntStream.range(0, partitions).mapToObj(String::valueOf).toArray(String[]::new); + List rks = routingKeyStrategy.apply(name, partitions); + Assert.state(rks.size() == partitions, () -> "Expected " + partitions + " routing keys, not " + rks.size()); declarables.add(new DirectExchange(name, true, false, Map.of("x-super-stream", true))); for (int i = 0; i < partitions; i++) { - String rk = rks[i]; - Queue q = new Queue(name + "-" + rk, true, false, false, Map.of("x-queue-type", "stream")); + String rk = rks.get(i); + Queue q = new Queue(name + "-" + i, true, false, false, Map.of("x-queue-type", "stream")); declarables.add(q); declarables.add(new Binding(q.getName(), DestinationType.QUEUE, name, rk, Map.of("x-stream-partition-order", i))); diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java index e9d3a28ced..2dd1a4614e 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java @@ -24,6 +24,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import org.junit.jupiter.api.Test; @@ -69,9 +71,9 @@ void superStream(@Autowired ApplicationContext context, @Autowired RabbitTemplat container2.start(); StreamListenerContainer container3 = context.getBean(StreamListenerContainer.class, env, "three"); container3.start(); - template.convertAndSend("ss.sac.test", "0", "foo"); - template.convertAndSend("ss.sac.test", "1", "bar"); - template.convertAndSend("ss.sac.test", "2", "baz"); + template.convertAndSend("ss.sac.test", "rk-0", "foo"); + template.convertAndSend("ss.sac.test", "rk-1", "bar"); + template.convertAndSend("ss.sac.test", "rk-2", "baz"); assertThat(config.latch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(config.messages.keySet()).contains("one", "two", "three"); assertThat(config.info).contains("one:foo", "two:bar", "three:baz"); @@ -112,7 +114,9 @@ RabbitTemplate template(ConnectionFactory cf) { @Bean SuperStream superStream() { - return new SuperStream("ss.sac.test", 3); + return new SuperStream("ss.sac.test", 3, (q, i) -> IntStream.range(0, i) + .mapToObj(j -> "rk-" + j) + .collect(Collectors.toList())); } @Bean From 62212639c373a2e4aed666968b2b84b2619755f7 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 26 Sep 2022 16:05:42 -0400 Subject: [PATCH 156/737] GH-1465: Polish Super Stream Javadocs --- .../rabbit/stream/listener/StreamListenerContainer.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java index 64cd4ccdcf..749492bf34 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java @@ -90,6 +90,10 @@ public StreamListenerContainer(Environment environment, @Nullable Codec codec) { this.streamConverter = new DefaultStreamMessageConverter(codec); } + /** + * {@inheritDoc} + * Mutually exclusive with {@link #superStream(String, String)}. + */ @Override public void setQueueNames(String... queueNames) { Assert.isTrue(queueNames != null && queueNames.length == 1, "Only one stream is supported"); @@ -98,6 +102,7 @@ public void setQueueNames(String... queueNames) { /** * Enable Single Active Consumer on a Super Stream. + * Mutually exclusive with {@link #setQueueNames(String...)}. * @param superStream the stream. * @param name the consumer name. * @since 3.0 From f54f8fb37f20303a97dbd69b605b0249782fd232 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 26 Sep 2022 16:08:03 -0400 Subject: [PATCH 157/737] Make artifactoryPublish dependsOn build --- publish-maven.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/publish-maven.gradle b/publish-maven.gradle index 336bbc582e..ff0b457510 100644 --- a/publish-maven.gradle +++ b/publish-maven.gradle @@ -72,5 +72,6 @@ publishing { } artifactoryPublish { + dependsOn build publications(publishing.publications.mavenJava) } From 732f0da28413183f0c1e302875dbbeadc317523a Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 26 Sep 2022 18:32:36 -0400 Subject: [PATCH 158/737] GH-1465: Super Stream Support in Template --- .../stream/producer/RabbitStreamTemplate.java | 21 ++++++++++- .../producer/RabbitStreamTemplateTests.java | 25 +++++++++++++ src/reference/asciidoc/stream.adoc | 36 +++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java index e8483935fb..418c22bf37 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java @@ -17,6 +17,7 @@ package org.springframework.rabbit.stream.producer; import java.util.concurrent.CompletableFuture; +import java.util.function.Function; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessagePostProcessor; @@ -52,6 +53,8 @@ public class RabbitStreamTemplate implements RabbitStreamOperations, BeanNameAwa private final String streamName; + private Function superStreamRouting; + private MessageConverter messageConverter = new SimpleMessageConverter(); private StreamMessageConverter streamConverter = new DefaultStreamMessageConverter(); @@ -80,7 +83,13 @@ public RabbitStreamTemplate(Environment environment, String streamName) { private synchronized Producer createOrGetProducer() { if (this.producer == null) { ProducerBuilder builder = this.environment.producerBuilder(); - builder.stream(this.streamName); + if (this.superStreamRouting == null) { + builder.stream(this.streamName); + } + else { + builder.superStream(this.streamName) + .routing(this.superStreamRouting); + } this.producerCustomizer.accept(this.beanName, builder); this.producer = builder.build(); if (!this.streamConverterSet) { @@ -96,6 +105,16 @@ public synchronized void setBeanName(String name) { this.beanName = name; } + /** + * Add a routing function, making the stream a super stream. + * @param superStreamRouting the routing function. + * @since 3.0 + */ + public void setSuperStreamRouting(Function superStreamRouting) { + this.superStreamRouting = superStreamRouting; + } + + /** * Set a converter for {@link #convertAndSend(Object)} operations. * @param messageConverter the converter. diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplateTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplateTests.java index b0bb26414d..9d06f42abe 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplateTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplateTests.java @@ -22,6 +22,8 @@ import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willAnswer; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @@ -114,4 +116,27 @@ void handleConfirm() throws InterruptedException, ExecutionException { } } + @Test + void superStream() { + Environment env = mock(Environment.class); + ProducerBuilder pb = mock(ProducerBuilder.class); + given(pb.superStream(any())).willReturn(pb); + given(env.producerBuilder()).willReturn(pb); + Producer producer = mock(Producer.class); + given(pb.build()).willReturn(producer); + try (RabbitStreamTemplate template = new RabbitStreamTemplate(env, "foo")) { + SimpleMessageConverter messageConverter = new SimpleMessageConverter(); + template.setMessageConverter(messageConverter); + assertThat(template.messageConverter()).isSameAs(messageConverter); + StreamMessageConverter converter = mock(StreamMessageConverter.class); + given(converter.fromMessage(any())).willReturn(mock(Message.class)); + template.setStreamConverter(converter); + template.setSuperStreamRouting(msg -> "bar"); + template.convertAndSend("x"); + verify(pb).superStream("foo"); + verify(pb).routing(any()); + verify(pb, never()).stream("foo"); + } + } + } diff --git a/src/reference/asciidoc/stream.adoc b/src/reference/asciidoc/stream.adoc index 8de6ca0596..6adac16561 100644 --- a/src/reference/asciidoc/stream.adoc +++ b/src/reference/asciidoc/stream.adoc @@ -180,6 +180,42 @@ SuperStream superStream() { The `RabbitAdmin` detects this bean and will declare the exchange (`my.super.stream`) and 3 queues (partitions) - `my.super-stream-n` where `n` is `0`, `1`, `2`, bound with routing keys equal to `n`. +If you also wish to publish over AMQP to the exchange, you can provide custom routing keys: + +==== +[source, java] +---- +@Bean +SuperStream superStream() { + return new SuperStream("my.super.stream", 3, (q, i) -> IntStream.range(0, i) + .mapToObj(j -> "rk-" + j) + .collect(Collectors.toList())); +} +---- +==== + +The number of keys must equal the number of partitions. + +===== Producing to a SuperStream + +You must add a `superStreamRoutingFunction` to the `RabbitStreamTemplate`: + +==== +[source, java] +---- +@Bean +RabbitStreamTemplate streamTemplate(Environment env) { + RabbitStreamTemplate template = new RabbitStreamTemplate(env, "stream.queue1"); + template.setSuperStreamRouting(message -> { + // some logic to return a String for the client's hashing algorithm + }); + return template; +} +---- +==== + +You can also publish over AMQP, using the `RabbitTemplate`. + ===== Consuming Super Streams with Single Active Consumers Invoke the `superStream` method on the listener container to enable a single active consumer on a super stream. From 4ee9d21a579a15de9490de7b5070cc03d8ba2e55 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 28 Sep 2022 09:15:33 -0400 Subject: [PATCH 159/737] Upgrade stream-client, Docker Image --- build.gradle | 2 +- .../stream/support/AbstractIntegrationTests.java | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 011254552c..80dc2cb195 100644 --- a/build.gradle +++ b/build.gradle @@ -58,7 +58,7 @@ ext { micrometerVersion = '1.10.0-M6' micrometerTracingVersion = '1.0.0-M8' mockitoVersion = '4.8.0' - rabbitmqStreamVersion = '0.7.0' + rabbitmqStreamVersion = '0.8.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.16.0' rabbitmqHttpClientVersion = '3.12.1' reactorVersion = '2022.0.0-M6' diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/AbstractIntegrationTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/AbstractIntegrationTests.java index e0c7f04dd7..f38b685cbb 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/AbstractIntegrationTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/AbstractIntegrationTests.java @@ -18,8 +18,9 @@ import java.time.Duration; +import org.junit.jupiter.api.AfterAll; import org.testcontainers.containers.GenericContainer; -import org.testcontainers.utility.DockerImageName; +import org.testcontainers.containers.RabbitMQContainer; /** * @author Gary Russell @@ -33,13 +34,14 @@ public abstract class AbstractIntegrationTests { static { if (System.getProperty("spring.rabbit.use.local.server") == null && System.getenv("SPRING_RABBIT_USE_LOCAL_SERVER") == null) { - String image = "pivotalrabbitmq/rabbitmq-stream"; + String image = "rabbitmq:3.11"; String cache = System.getenv().get("IMAGE_CACHE"); if (cache != null) { image = cache + image; } - RABBITMQ = new GenericContainer<>(DockerImageName.parse(image)) + RABBITMQ = new RabbitMQContainer(image) .withExposedPorts(5672, 15672, 5552) + .withPluginsEnabled("rabbitmq_stream", "rabbitmq_management") .withStartupTimeout(Duration.ofMinutes(2)); RABBITMQ.start(); } @@ -60,4 +62,9 @@ public static int streamPort() { return RABBITMQ != null ? RABBITMQ.getMappedPort(5552) : 5552; } + @AfterAll + static void shutDown() { + RABBITMQ.close(); + } + } From 0cce53331a5db1859fa051e6c0de5e4663f66da3 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 28 Sep 2022 12:14:24 -0400 Subject: [PATCH 160/737] Fix Stream TestContainer - do not close the container; used by other tests. --- .../stream/support/AbstractIntegrationTests.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/AbstractIntegrationTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/AbstractIntegrationTests.java index f38b685cbb..a8bf3cffe2 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/AbstractIntegrationTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/AbstractIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-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. @@ -18,7 +18,6 @@ import java.time.Duration; -import org.junit.jupiter.api.AfterAll; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.RabbitMQContainer; @@ -43,28 +42,29 @@ public abstract class AbstractIntegrationTests { .withExposedPorts(5672, 15672, 5552) .withPluginsEnabled("rabbitmq_stream", "rabbitmq_management") .withStartupTimeout(Duration.ofMinutes(2)); + System.out.println("Created"); RABBITMQ.start(); + System.out.println("Started"); } else { RABBITMQ = null; } + System.out.println(RABBITMQ); } public static int amqpPort() { + System.out.println("amqp:" + RABBITMQ); return RABBITMQ != null ? RABBITMQ.getMappedPort(5672) : 5672; } public static int managementPort() { + System.out.println("mgmt:" + RABBITMQ); return RABBITMQ != null ? RABBITMQ.getMappedPort(15672) : 15672; } public static int streamPort() { + System.out.println("stream:" + RABBITMQ); return RABBITMQ != null ? RABBITMQ.getMappedPort(5552) : 5552; } - @AfterAll - static void shutDown() { - RABBITMQ.close(); - } - } From e0c6568857d01d0c3593340ceca2f12c18726073 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 28 Sep 2022 12:16:57 -0400 Subject: [PATCH 161/737] Fix AbstractIntegrationTests Remove system.outs. --- .../rabbit/stream/support/AbstractIntegrationTests.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/AbstractIntegrationTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/AbstractIntegrationTests.java index a8bf3cffe2..7afd67fd02 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/AbstractIntegrationTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/AbstractIntegrationTests.java @@ -42,28 +42,22 @@ public abstract class AbstractIntegrationTests { .withExposedPorts(5672, 15672, 5552) .withPluginsEnabled("rabbitmq_stream", "rabbitmq_management") .withStartupTimeout(Duration.ofMinutes(2)); - System.out.println("Created"); RABBITMQ.start(); - System.out.println("Started"); } else { RABBITMQ = null; } - System.out.println(RABBITMQ); } public static int amqpPort() { - System.out.println("amqp:" + RABBITMQ); return RABBITMQ != null ? RABBITMQ.getMappedPort(5672) : 5672; } public static int managementPort() { - System.out.println("mgmt:" + RABBITMQ); return RABBITMQ != null ? RABBITMQ.getMappedPort(15672) : 15672; } public static int streamPort() { - System.out.println("stream:" + RABBITMQ); return RABBITMQ != null ? RABBITMQ.getMappedPort(5552) : 5552; } From 52e49cba803fb3dee7cb759aa501573a0214d659 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 28 Sep 2022 12:36:25 -0400 Subject: [PATCH 162/737] Fix Stream RabbitListenerTests - stop container before deleting the streams. --- .../rabbit/stream/listener/RabbitListenerTests.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java index cd6d363bc8..0b312bf461 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java @@ -176,6 +176,13 @@ private void clean(Environment env) { public boolean isRunning() { return this.running; } + + @Override + public int getPhase() { + return 0; + } + + }; } From 506abd5e9cbac9c6ee11c803c773381c1533a6f0 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 4 Oct 2022 11:59:01 -0400 Subject: [PATCH 163/737] GH-1509: Add Concurrency for Super Streams Resolves https://github.com/spring-projects/spring-amqp/issues/1509 --- .../listener/StreamListenerContainer.java | 73 +++++++--- .../SuperStreamConcurrentSACTests.java | 133 ++++++++++++++++++ .../support/AbstractIntegrationTests.java | 2 +- src/reference/asciidoc/amqp.adoc | 5 +- src/reference/asciidoc/stream.adoc | 6 +- 5 files changed, 198 insertions(+), 21 deletions(-) create mode 100644 spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamConcurrentSACTests.java diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java index 749492bf34..33926f5e3c 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java @@ -16,10 +16,11 @@ package org.springframework.rabbit.stream.listener; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import org.aopalliance.aop.Advice; -import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.amqp.core.Message; @@ -29,6 +30,7 @@ import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.beans.factory.BeanNameAware; +import org.springframework.core.log.LogAccessor; import org.springframework.lang.Nullable; import org.springframework.rabbit.stream.support.StreamMessageProperties; import org.springframework.rabbit.stream.support.converter.DefaultStreamMessageConverter; @@ -49,15 +51,21 @@ */ public class StreamListenerContainer implements MessageListenerContainer, BeanNameAware { - protected Log logger = LogFactory.getLog(getClass()); // NOSONAR + protected LogAccessor logger = new LogAccessor(LogFactory.getLog(getClass())); // NOSONAR private final ConsumerBuilder builder; + private final Collection consumers = new ArrayList<>(); + private StreamMessageConverter streamConverter; private ConsumerCustomizer consumerCustomizer = (id, con) -> { }; - private Consumer consumer; + private boolean simpleStream; + + private boolean superStream; + + private int concurrency = 1; private String listenerId; @@ -96,22 +104,41 @@ public StreamListenerContainer(Environment environment, @Nullable Codec codec) { */ @Override public void setQueueNames(String... queueNames) { + Assert.isTrue(!this.superStream, "setQueueNames() and superStream() are mutually exclusive"); Assert.isTrue(queueNames != null && queueNames.length == 1, "Only one stream is supported"); this.builder.stream(queueNames[0]); + this.simpleStream = true; + } + + /** + * Enable Single Active Consumer on a Super Stream, with one consumer. + * Mutually exclusive with {@link #setQueueNames(String...)}. + * @param streamName the stream. + * @param name the consumer name. + * @since 3.0 + */ + public void superStream(String streamName, String name) { + superStream(streamName, name, 1); } /** - * Enable Single Active Consumer on a Super Stream. + * Enable Single Active Consumer on a Super Stream with the provided number of consumers. + * There must be at least that number of partitions in the Super Stream. * Mutually exclusive with {@link #setQueueNames(String...)}. - * @param superStream the stream. + * @param streamName the stream. * @param name the consumer name. + * @param consumers the number of consumers. * @since 3.0 */ - public void superStream(String superStream, String name) { - Assert.notNull(superStream, "'superStream' cannot be null"); - this.builder.superStream(superStream) + public void superStream(String streamName, String name, int consumers) { + Assert.isTrue(consumers > 0, () -> "'concurrency' must be greater than zero, not " + consumers); + this.concurrency = consumers; + Assert.isTrue(!this.simpleStream, "setQueueNames() and superStream() are mutually exclusive"); + Assert.notNull(streamName, "'superStream' cannot be null"); + this.builder.superStream(streamName) .singleActiveConsumer() .name(name); + this.superStream = true; } /** @@ -201,23 +228,35 @@ public Object getMessageListener() { @Override public synchronized boolean isRunning() { - return this.consumer != null; + return this.consumers.size() > 0; } @Override public synchronized void start() { - if (this.consumer == null) { + if (this.consumers.size() == 0) { this.consumerCustomizer.accept(getListenerId(), this.builder); - this.consumer = this.builder.build(); + if (this.simpleStream) { + this.consumers.add(this.builder.build()); + } + else { + for (int i = 0; i < this.concurrency; i++) { + this.consumers.add(this.builder.build()); + } + } } } @Override public synchronized void stop() { - if (this.consumer != null) { - this.consumer.close(); - this.consumer = null; - } + this.consumers.forEach(consumer -> { + try { + consumer.close(); + } + catch (RuntimeException ex) { + this.logger.error(ex, "Failed to close consumer"); + } + }); + this.consumers.clear(); } @Override @@ -233,8 +272,8 @@ public void setupMessageListener(MessageListener messageListener) { try { ((ChannelAwareMessageListener) this.messageListener).onMessage(message2, null); } - catch (Exception e) { // NOSONAR - this.logger.error("Listner threw an exception", e); + catch (Exception ex) { // NOSONAR + this.logger.error(ex, "Listner threw an exception"); } } else { diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamConcurrentSACTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamConcurrentSACTests.java new file mode 100644 index 0000000000..f089a2ca8b --- /dev/null +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamConcurrentSACTests.java @@ -0,0 +1,133 @@ +/* + * 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.rabbit.stream.listener; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.core.Declarables; +import org.springframework.amqp.core.DirectExchange; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.rabbit.stream.config.SuperStream; +import org.springframework.rabbit.stream.support.AbstractIntegrationTests; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import com.rabbitmq.stream.Address; +import com.rabbitmq.stream.Environment; +import com.rabbitmq.stream.OffsetSpecification; + +/** + * @author Gary Russell + * @since 3.0 + * + */ +@SpringJUnitConfig +public class SuperStreamConcurrentSACTests extends AbstractIntegrationTests { + + @Test + void concurrent(@Autowired StreamListenerContainer container, @Autowired RabbitTemplate template, + @Autowired Config config, @Autowired RabbitAdmin admin, + @Autowired Declarables superStream) throws InterruptedException { + + template.getConnectionFactory().createConnection(); + container.start(); + assertThat(config.consumerLatch.await(10, TimeUnit.SECONDS)).isTrue(); + template.convertAndSend("ss.sac.concurrency.test", "0", "foo"); + template.convertAndSend("ss.sac.concurrency.test", "1", "bar"); + template.convertAndSend("ss.sac.concurrency.test", "2", "baz"); + assertThat(config.messageLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(config.threads).hasSize(3); + container.stop(); + clean(admin, superStream); + } + + private void clean(RabbitAdmin admin, Declarables declarables) { + declarables.getDeclarablesByType(Queue.class).forEach(queue -> admin.deleteQueue(queue.getName())); + declarables.getDeclarablesByType(DirectExchange.class).forEach(ex -> admin.deleteExchange(ex.getName())); + } + + @Configuration + public static class Config { + + final Set threads = new HashSet<>(); + + final CountDownLatch consumerLatch = new CountDownLatch(3); + + final CountDownLatch messageLatch = new CountDownLatch(3); + + @Bean + CachingConnectionFactory cf() { + return new CachingConnectionFactory("localhost", amqpPort()); + } + + @Bean + RabbitAdmin admin(ConnectionFactory cf) { + return new RabbitAdmin(cf); + } + + @Bean + RabbitTemplate template(ConnectionFactory cf) { + return new RabbitTemplate(cf); + } + + @Bean + SuperStream superStream() { + return new SuperStream("ss.sac.concurrency.test", 3); + } + + @Bean + static Environment environment() { + return Environment.builder() + .addressResolver(add -> new Address("localhost", streamPort())) + .maxConsumersByConnection(1) + .build(); + } + + @Bean + StreamListenerContainer concurrentContainer(Environment env) { + StreamListenerContainer container = new StreamListenerContainer(env); + container.superStream("ss.sac.concurrency.test", "concurrent", 3); + container.setupMessageListener(msg -> { + this.threads.add(Thread.currentThread().getName()); + this.messageLatch.countDown(); + }); + container.setConsumerCustomizer((id, builder) -> { + builder.consumerUpdateListener(context -> { + this.consumerLatch.countDown(); + return OffsetSpecification.last(); + }); + }); + container.setAutoStartup(false); + return container; + } + + } + +} diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/AbstractIntegrationTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/AbstractIntegrationTests.java index 7afd67fd02..9b6f1575d5 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/AbstractIntegrationTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/AbstractIntegrationTests.java @@ -33,7 +33,7 @@ public abstract class AbstractIntegrationTests { static { if (System.getProperty("spring.rabbit.use.local.server") == null && System.getenv("SPRING_RABBIT_USE_LOCAL_SERVER") == null) { - String image = "rabbitmq:3.11"; + String image = "rabbitmq:3.11-management"; String cache = System.getenv().get("IMAGE_CACHE"); if (cache != null) { image = cache + image; diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index 9095eff965..0c83b0a3b6 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -5741,7 +5741,7 @@ Enable this feature by calling `ConnectionFactoryUtils.enableAfterCompletionFail ==== Message Listener Container Configuration There are quite a few options for configuring a `SimpleMessageListenerContainer` (SMLC) and a `DirectMessageListenerContainer` (DMLC) related to transactions and quality of service, and some of them interact with each other. -Properties that apply to the SMLC, DMLC, or `StreamListenerContainer` (StLC) (see <> are indicated by the check mark in the appropriate column. +Properties that apply to the SMLC, DMLC, or `StreamListenerContainer` (StLC) (see <>) are indicated by the check mark in the appropriate column. See <> for information to help you decide which container is appropriate for your application. The following table shows the container property names and their equivalent attribute names (in parentheses) when using the namespace to configure a ``. @@ -5894,10 +5894,11 @@ a| |The number of concurrent consumers to initially start for each listener. See <>. +For the `StLC`, concurrency is controlled via an overloaded `superStream` method; see <>. a|image::images/tickmark.png[] a| -a| +a|image::images/tickmark.png[] |[[connectionFactory]]<> + (connection-factory) diff --git a/src/reference/asciidoc/stream.adoc b/src/reference/asciidoc/stream.adoc index 6adac16561..01a72ccdd3 100644 --- a/src/reference/asciidoc/stream.adoc +++ b/src/reference/asciidoc/stream.adoc @@ -216,6 +216,7 @@ RabbitStreamTemplate streamTemplate(Environment env) { You can also publish over AMQP, using the `RabbitTemplate`. +[[super-stream-consumer]] ===== Consuming Super Streams with Single Active Consumers Invoke the `superStream` method on the listener container to enable a single active consumer on a super stream. @@ -227,7 +228,7 @@ Invoke the `superStream` method on the listener container to enable a single act @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) StreamListenerContainer container(Environment env, String name) { StreamListenerContainer container = new StreamListenerContainer(env); - container.superStream("ss.sac", "myConsumer"); + container.superStream("ss.sac", "myConsumer", 3); // concurrency = 3 container.setupMessageListener(msg -> { ... }); @@ -236,3 +237,6 @@ StreamListenerContainer container(Environment env, String name) { } ---- ==== + +IMPORTANT: At this time, when the concurrency is greater than 1, the actual concurrency is further controlled by the `Environment`; to achieve full concurrency, set the environment's `maxConsumersByConnection` to 1. +See https://rabbitmq.github.io/rabbitmq-stream-java-client/snapshot/htmlsingle/#configuring-the-environment[Configuring the Environment]. From 639eb160eed42464ce43d18a99ad1aeb4049310c Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 6 Oct 2022 14:14:38 -0400 Subject: [PATCH 164/737] GH-1444: Observablility Documentation Resolves https://github.com/spring-projects/spring-amqp/issues/1507 Refactor for latest snapshots; add documentation generation. Use NOOP registry and supplier for context. Also fix class tangles. Disable auto doc generation and polish manually. * Fix observation of batch listeners; disable more tests temporarily. * Add remoteServiceName to contexts; fix race in test. * Add tests for remoteServiceName. * Set the service name instead of overriding the getter. * Re-enable tests. * Switch to Spring Snapshots, Revert "Re-enable tests." This reverts commit e0a80e372fe0e5954ddc734e2fbbf5aabe457fc9. * Disable logback adjuster. * Disable logback appender tests. --- build.gradle | 49 ++++++++++++++++--- .../amqp/rabbit/junit/JUnitUtils.java | 18 +++---- .../stream/listener/RabbitListenerTests.java | 2 + .../rabbit/connection/RabbitAccessor.java | 4 +- .../amqp/rabbit/core/RabbitTemplate.java | 16 ++---- .../AbstractMessageListenerContainer.java | 19 +++---- ...ltRabbitListenerObservationConvention.java | 47 ------------------ ...ltRabbitTemplateObservationConvention.java | 47 ------------------ .../micrometer/RabbitListenerObservation.java | 29 ++++++++++- .../RabbitMessageReceiverContext.java | 4 +- .../RabbitMessageSenderContext.java | 1 + .../micrometer/RabbitTemplateObservation.java | 31 ++++++++++-- .../EnableRabbitIntegrationTests.java | 2 + ...ueueConnectionFactoryIntegrationTests.java | 5 +- .../core/FixedReplyQueueDeadLetterTests.java | 4 +- .../core/RabbitAdminIntegrationTests.java | 4 +- .../amqp/rabbit/core/RabbitAdminTests.java | 4 +- .../amqp/rabbit/core/RabbitRestApiTests.java | 4 +- .../logback/AmqpAppenderIntegrationTests.java | 2 + .../rabbit/logback/AmqpAppenderTests.java | 2 + .../ObservationIntegrationTests.java | 31 ++++++++---- .../support/micrometer/ObservationTests.java | 4 +- src/reference/asciidoc/_conventions.adoc | 11 +++++ src/reference/asciidoc/_metrics.adoc | 44 +++++++++++++++++ src/reference/asciidoc/_spans.adoc | 38 ++++++++++++++ src/reference/asciidoc/amqp.adoc | 6 ++- src/reference/asciidoc/appendix.adoc | 11 +++++ src/reference/asciidoc/index.adoc | 1 - 28 files changed, 283 insertions(+), 157 deletions(-) delete mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/DefaultRabbitListenerObservationConvention.java delete mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/DefaultRabbitTemplateObservationConvention.java create mode 100644 src/reference/asciidoc/_conventions.adoc create mode 100644 src/reference/asciidoc/_metrics.adoc create mode 100644 src/reference/asciidoc/_spans.adoc diff --git a/build.gradle b/build.gradle index 80dc2cb195..e1874a35a6 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,6 @@ buildscript { ext.kotlinVersion = '1.7.0' + ext.isCI = System.getenv('GITHUB_ACTION') || System.getenv('bamboo_buildKey') repositories { mavenCentral() gradlePluginPortal() @@ -55,17 +56,18 @@ ext { log4jVersion = '2.18.0' logbackVersion = '1.2.3' lz4Version = '1.8.0' - micrometerVersion = '1.10.0-M6' - micrometerTracingVersion = '1.0.0-M8' + micrometerDocsVersion = '1.0.0-SNAPSHOT' + micrometerVersion = '1.10.0-SNAPSHOT' + micrometerTracingVersion = '1.0.0-SNAPSHOT' mockitoVersion = '4.8.0' rabbitmqStreamVersion = '0.8.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.16.0' rabbitmqHttpClientVersion = '3.12.1' - reactorVersion = '2022.0.0-M6' + reactorVersion = '2022.0.0-SNAPSHOT' snappyVersion = '1.1.8.4' - springDataVersion = '2022.0.0-M6' - springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.0-M6' - springRetryVersion = '2.0.0-M1' + springDataVersion = '2022.0.0-SNAPSHOT' + springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.0-SNAPSHOT' + springRetryVersion = '2.0.0-SNAPSHOT' zstdJniVersion = '1.5.0-2' } @@ -221,7 +223,7 @@ subprojects { subproject -> } task updateCopyrights { - onlyIf { !System.getenv('GITHUB_ACTION') && !System.getenv('bamboo_buildKey') } + onlyIf { !isCI } inputs.files(modifiedFiles.filter { f -> f.path.contains(subproject.name) }) outputs.dir('build/classes') @@ -374,6 +376,10 @@ project('spring-amqp') { project('spring-rabbit') { description = 'Spring RabbitMQ Support' + configurations { + adoc + } + dependencies { api project(':spring-amqp') @@ -411,8 +417,37 @@ project('spring-rabbit') { testRuntimeOnly 'com.fasterxml.jackson.module:jackson-module-kotlin' testRuntimeOnly ("junit:junit:$junit4Version") { exclude group: 'org.hamcrest', module: 'hamcrest-core' + + adoc "io.micrometer:micrometer-docs-generator-spans:$micrometerDocsVersion" + adoc "io.micrometer:micrometer-docs-generator-metrics:$micrometerDocsVersion" + + } + + def inputDir = file('src/main/java/org/springframework/amqp/rabbit/support/micrometer').absolutePath + def outputDir = rootProject.file('src/reference/asciidoc').absolutePath + + task generateObservabilityMetricsDocs(type: JavaExec) { + onlyIf { !isCI } + mainClass = 'io.micrometer.docs.metrics.DocsFromSources' + inputs.dir(inputDir) + outputs.dir(outputDir) + classpath configurations.adoc + args inputDir, '.*', outputDir } + task generateObservabilitySpansDocs(type: JavaExec) { + onlyIf { !isCI } + mainClass = 'io.micrometer.docs.spans.DocsFromSources' + inputs.dir(inputDir) + outputs.dir(outputDir) + classpath configurations.adoc + args inputDir, '.*', outputDir + } + + // javadoc { + // finalizedBy generateObservabilityMetricsDocs, generateObservabilitySpansDocs + // } + } compileTestKotlin { diff --git a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/JUnitUtils.java b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/JUnitUtils.java index 649476155e..6eedd301e3 100644 --- a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/JUnitUtils.java +++ b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/JUnitUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-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. @@ -29,7 +29,6 @@ import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.LoggerConfig; -import org.slf4j.LoggerFactory; /** * Utility methods for JUnit rules and conditions. @@ -109,11 +108,12 @@ public static LevelsContainer adjustLogLevels(String methodName, List> ctx.updateLoggers(); Map oldLbLevels = new HashMap<>(); - categories.forEach(cat -> { - ch.qos.logback.classic.Logger lbLogger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(cat); - oldLbLevels.put(cat, lbLogger.getLevel()); - lbLogger.setLevel(ch.qos.logback.classic.Level.toLevel(level.name())); - }); +// TODO: Fix +// categories.forEach(cat -> { +// ch.qos.logback.classic.Logger lbLogger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(cat); +// oldLbLevels.put(cat, lbLogger.getLevel()); +// lbLogger.setLevel(ch.qos.logback.classic.Level.toLevel(level.name())); +// }); LOGGER.info("++++++++++++++++++++++++++++ " + "Overridden log level setting for: " + classes.stream() @@ -137,8 +137,8 @@ public static void revertLevels(String methodName, LevelsContainer container) { ((Logger) LogManager.getLogger(key)).setLevel(value); } }); - container.oldLbLevels.forEach((key, value) -> - ((ch.qos.logback.classic.Logger) LoggerFactory.getLogger(key)).setLevel(value)); +// container.oldLbLevels.forEach((key, value) -> +// ((ch.qos.logback.classic.Logger) LoggerFactory.getLogger(key)).setLevel(value)); } public static class LevelsContainer { diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java index 0b312bf461..fa5f47f82c 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java @@ -25,6 +25,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Queue; @@ -99,6 +100,7 @@ void nativeMsg(@Autowired RabbitTemplate template) throws InterruptedException { } @Test + @Disabled("Temporary until SF uses Micrometer snaps") void queueOverAmqp() throws Exception { Client client = new Client("http://guest:guest@localhost:" + managementPort() + "/api"); QueueInfo queue = client.getQueue("/", "stream.created.over.amqp"); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java index 54c23bf687..b8c23f630f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java @@ -43,7 +43,7 @@ public abstract class RabbitAccessor implements InitializingBean { private volatile boolean transactional; - private ObservationRegistry observationRegistry; + private ObservationRegistry observationRegistry = ObservationRegistry.NOOP; public boolean isChannelTransacted() { return this.transactional; @@ -119,7 +119,7 @@ protected RuntimeException convertRabbitAccessException(Exception ex) { } protected void obtainObservationRegistry(@Nullable ApplicationContext appContext) { - if (this.observationRegistry == null && appContext != null) { + if (appContext != null) { ObjectProvider registry = appContext.getBeanProvider(ObservationRegistry.class); this.observationRegistry = registry.getIfUnique(); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java index 2bbd7b98c5..639d74163a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java @@ -74,9 +74,9 @@ import org.springframework.amqp.rabbit.support.MessagePropertiesConverter; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.amqp.rabbit.support.ValueExpression; -import org.springframework.amqp.rabbit.support.micrometer.DefaultRabbitTemplateObservationConvention; import org.springframework.amqp.rabbit.support.micrometer.RabbitMessageSenderContext; import org.springframework.amqp.rabbit.support.micrometer.RabbitTemplateObservation; +import org.springframework.amqp.rabbit.support.micrometer.RabbitTemplateObservation.DefaultRabbitTemplateObservationConvention; import org.springframework.amqp.rabbit.support.micrometer.RabbitTemplateObservationConvention; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.amqp.support.converter.SimpleMessageConverter; @@ -2428,21 +2428,15 @@ public void doSend(Channel channel, String exchangeArg, String routingKeyArg, Me protected void observeTheSend(Channel channel, Message message, boolean mandatory, String exch, String rKey) { - if (!this.observationRegistryObtained) { + if (!this.observationRegistryObtained && this.observationEnabled) { obtainObservationRegistry(this.applicationContext); this.observationRegistryObtained = true; } - Observation observation; ObservationRegistry registry = getObservationRegistry(); - if (!this.observationEnabled || registry == null) { - observation = Observation.NOOP; - } - else { - observation = RabbitTemplateObservation.TEMPLATE_OBSERVATION.observation(this.observationConvention, - DefaultRabbitTemplateObservationConvention.INSTANCE, - new RabbitMessageSenderContext(message, this.beanName, exch + "/" + rKey), registry); + Observation observation = RabbitTemplateObservation.TEMPLATE_OBSERVATION.observation(this.observationConvention, + DefaultRabbitTemplateObservationConvention.INSTANCE, + () -> new RabbitMessageSenderContext(message, this.beanName, exch + "/" + rKey), registry); - } observation.observe(() -> sendToRabbit(channel, exch, rKey, mandatory, message)); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index e96633e56c..1ddd3a738e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -63,8 +63,8 @@ import org.springframework.amqp.rabbit.support.DefaultMessagePropertiesConverter; import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; import org.springframework.amqp.rabbit.support.MessagePropertiesConverter; -import org.springframework.amqp.rabbit.support.micrometer.DefaultRabbitListenerObservationConvention; import org.springframework.amqp.rabbit.support.micrometer.RabbitListenerObservation; +import org.springframework.amqp.rabbit.support.micrometer.RabbitListenerObservation.DefaultRabbitListenerObservationConvention; import org.springframework.amqp.rabbit.support.micrometer.RabbitListenerObservationConvention; import org.springframework.amqp.rabbit.support.micrometer.RabbitMessageReceiverContext; import org.springframework.amqp.support.ConditionalExceptionLogger; @@ -1435,7 +1435,9 @@ public void start() { } } } - obtainObservationRegistry(this.applicationContext); + if (this.observationEnabled) { + obtainObservationRegistry(this.applicationContext); + } try { logger.debug("Starting Rabbit listener container."); configureAdminIfNeeded(); @@ -1536,16 +1538,15 @@ protected void invokeErrorHandler(Throwable ex) { protected void executeListener(Channel channel, Object data) { Observation observation; ObservationRegistry registry = getObservationRegistry(); - if (!this.observationEnabled || data instanceof List || registry == null) { - observation = Observation.NOOP; - } - else { - Message message = (Message) data; + if (data instanceof Message message) { observation = RabbitListenerObservation.LISTENER_OBSERVATION.observation(this.observationConvention, DefaultRabbitListenerObservationConvention.INSTANCE, - new RabbitMessageReceiverContext(message, getListenerId()), registry); + () -> new RabbitMessageReceiverContext(message, getListenerId()), registry); + observation.observe(() -> executeListenerAndHandleException(channel, data)); + } + else { + executeListenerAndHandleException(channel, data); } - observation.observe(() -> executeListenerAndHandleException(channel, data)); } @SuppressWarnings(UNCHECKED) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/DefaultRabbitListenerObservationConvention.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/DefaultRabbitListenerObservationConvention.java deleted file mode 100644 index ae6f4488b9..0000000000 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/DefaultRabbitListenerObservationConvention.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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.support.micrometer; - -import io.micrometer.common.KeyValues; - -/** - * Default {@link RabbitListenerObservationConvention} for Rabbit listener key values. - * - * @author Gary Russell - * @since 3.0 - * - */ -public class DefaultRabbitListenerObservationConvention implements RabbitListenerObservationConvention { - - /** - * A singleton instance of the convention. - */ - public static final DefaultRabbitListenerObservationConvention INSTANCE = - new DefaultRabbitListenerObservationConvention(); - - @Override - public KeyValues getLowCardinalityKeyValues(RabbitMessageReceiverContext context) { - return KeyValues.of(RabbitListenerObservation.ListenerLowCardinalityTags.LISTENER_ID.asString(), - context.getListenerId()); - } - - @Override - public String getContextualName(RabbitMessageReceiverContext context) { - return context.getSource() + " receive"; - } - -} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/DefaultRabbitTemplateObservationConvention.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/DefaultRabbitTemplateObservationConvention.java deleted file mode 100644 index 285d52b835..0000000000 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/DefaultRabbitTemplateObservationConvention.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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.support.micrometer; - -import io.micrometer.common.KeyValues; - -/** - * Default {@link RabbitTemplateObservationConvention} for Rabbit template key values. - * - * @author Gary Russell - * @since 3.0 - * - */ -public class DefaultRabbitTemplateObservationConvention implements RabbitTemplateObservationConvention { - - /** - * A singleton instance of the convention. - */ - public static final DefaultRabbitTemplateObservationConvention INSTANCE = - new DefaultRabbitTemplateObservationConvention(); - - @Override - public KeyValues getLowCardinalityKeyValues(RabbitMessageSenderContext context) { - return KeyValues.of(RabbitTemplateObservation.TemplateLowCardinalityTags.BEAN_NAME.asString(), - context.getBeanName()); - } - - @Override - public String getContextualName(RabbitMessageSenderContext context) { - return context.getDestination() + " send"; - } - -} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java index 8454a987a3..b580bd2e7e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java @@ -16,10 +16,11 @@ package org.springframework.amqp.rabbit.support.micrometer; +import io.micrometer.common.KeyValues; import io.micrometer.common.docs.KeyName; import io.micrometer.observation.Observation.Context; import io.micrometer.observation.ObservationConvention; -import io.micrometer.observation.docs.DocumentedObservation; +import io.micrometer.observation.docs.ObservationDocumentation; /** * Spring Rabbit Observation for listeners. @@ -28,7 +29,7 @@ * @since 3.0 * */ -public enum RabbitListenerObservation implements DocumentedObservation { +public enum RabbitListenerObservation implements ObservationDocumentation { /** * Observation for Rabbit listeners. @@ -72,4 +73,28 @@ public String asString() { } + /** + * Default {@link RabbitListenerObservationConvention} for Rabbit listener key values. + */ + public static class DefaultRabbitListenerObservationConvention implements RabbitListenerObservationConvention { + + /** + * A singleton instance of the convention. + */ + public static final DefaultRabbitListenerObservationConvention INSTANCE = + new DefaultRabbitListenerObservationConvention(); + + @Override + public KeyValues getLowCardinalityKeyValues(RabbitMessageReceiverContext context) { + return KeyValues.of(RabbitListenerObservation.ListenerLowCardinalityTags.LISTENER_ID.asString(), + context.getListenerId()); + } + + @Override + public String getContextualName(RabbitMessageReceiverContext context) { + return context.getSource() + " receive"; + } + + } + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java index 07ffebd732..f37b56f1eb 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java @@ -17,6 +17,7 @@ package org.springframework.amqp.rabbit.support.micrometer; import org.springframework.amqp.core.Message; +import org.springframework.lang.Nullable; import io.micrometer.observation.transport.ReceiverContext; @@ -33,11 +34,12 @@ public class RabbitMessageReceiverContext extends ReceiverContext { private final Message message; - public RabbitMessageReceiverContext(Message message, String listenerId) { + public RabbitMessageReceiverContext(Message message, @Nullable String listenerId) { super((carrier, key) -> carrier.getMessageProperties().getHeader(key)); setCarrier(message); this.message = message; this.listenerId = listenerId; + setRemoteServiceName("RabbitMQ"); } public String getListenerId() { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageSenderContext.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageSenderContext.java index b1b25755d4..e327f6ebc6 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageSenderContext.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageSenderContext.java @@ -38,6 +38,7 @@ public RabbitMessageSenderContext(Message message, String beanName, String desti setCarrier(message); this.beanName = beanName; this.destination = destination; + setRemoteServiceName("RabbitMQ"); } public String getBeanName() { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservation.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservation.java index 01fb63f6c2..f3bc17f1e6 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservation.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservation.java @@ -16,10 +16,11 @@ package org.springframework.amqp.rabbit.support.micrometer; +import io.micrometer.common.KeyValues; import io.micrometer.common.docs.KeyName; import io.micrometer.observation.Observation.Context; import io.micrometer.observation.ObservationConvention; -import io.micrometer.observation.docs.DocumentedObservation; +import io.micrometer.observation.docs.ObservationDocumentation; /** * Spring RabbitMQ Observation for {@link org.springframework.amqp.rabbit.core.RabbitTemplate}. @@ -28,10 +29,10 @@ * @since 3.0 * */ -public enum RabbitTemplateObservation implements DocumentedObservation { +public enum RabbitTemplateObservation implements ObservationDocumentation { /** - * {@link org.springframework.kafka.core.KafkaTemplate} observation. + * Observation for RabbitTemplates. */ TEMPLATE_OBSERVATION { @@ -71,4 +72,28 @@ public String asString() { } + /** + * Default {@link RabbitTemplateObservationConvention} for Rabbit template key values. + */ + public static class DefaultRabbitTemplateObservationConvention implements RabbitTemplateObservationConvention { + + /** + * A singleton instance of the convention. + */ + public static final DefaultRabbitTemplateObservationConvention INSTANCE = + new DefaultRabbitTemplateObservationConvention(); + + @Override + public KeyValues getLowCardinalityKeyValues(RabbitMessageSenderContext context) { + return KeyValues.of(RabbitTemplateObservation.TemplateLowCardinalityTags.BEAN_NAME.asString(), + context.getBeanName()); + } + + @Override + public String getContextualName(RabbitMessageSenderContext context) { + return context.getDestination() + " send"; + } + + } + } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java index 84196b3697..45505f86be 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java @@ -47,6 +47,7 @@ import org.aopalliance.intercept.MethodInvocation; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -832,6 +833,7 @@ public void testHeadersExchange() throws Exception { } @Test + @Disabled("Temporary until SF uses Micrometer snaps") public void deadLetterOnDefaultExchange() { this.rabbitTemplate.convertAndSend("amqp656", "foo"); assertThat(this.rabbitTemplate.receiveAndConvert("amqp656dlq", 10000)).isEqualTo("foo"); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryIntegrationTests.java index 117c7f4765..24f1deb319 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-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. @@ -22,6 +22,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Queue; @@ -35,6 +36,7 @@ * @author Gary Russell */ @RabbitAvailable(management = true) +@Disabled("Temporary until SF uses Micrometer snaps") public class LocalizedQueueConnectionFactoryIntegrationTests { private LocalizedQueueConnectionFactory lqcf; @@ -61,6 +63,7 @@ public void tearDown() { } @Test + @Disabled("Temporary until SF uses Micrometer snaps") public void testConnect() throws Exception { RabbitAdmin admin = new RabbitAdmin(this.lqcf); Queue queue = new Queue(UUID.randomUUID().toString(), false, false, true); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/FixedReplyQueueDeadLetterTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/FixedReplyQueueDeadLetterTests.java index a79977dbfb..f7e862321d 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/FixedReplyQueueDeadLetterTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/FixedReplyQueueDeadLetterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 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. @@ -27,6 +27,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Binding; @@ -63,6 +64,7 @@ @SpringJUnitConfig @DirtiesContext @RabbitAvailable(management = true) +@Disabled("Temporary until SF uses Micrometer snaps") public class FixedReplyQueueDeadLetterTests { private static BrokerRunningSupport brokerRunning; diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminIntegrationTests.java index 1c504a7c78..85cdc2b05c 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -26,6 +26,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.amqp.AmqpIOException; @@ -62,6 +63,7 @@ * @author Artem Bilan */ @RabbitAvailable(management = true) +@Disabled("Temporary until SF uses Micrometer snaps") public class RabbitAdminIntegrationTests { private final CachingConnectionFactory connectionFactory = new CachingConnectionFactory(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java index 04b804ef48..d617d52773 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -46,6 +46,7 @@ import java.util.concurrent.TimeoutException; import org.apache.commons.logging.Log; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -99,6 +100,7 @@ * */ @RabbitAvailable(management = true) +@Disabled("Temporary until SF uses Micrometer snaps") public class RabbitAdminTests { @Test diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitRestApiTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitRestApiTests.java index 04cbf541d2..a4e5178f8e 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitRestApiTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitRestApiTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-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. @@ -29,6 +29,7 @@ import java.util.UUID; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.amqp.AmqpException; @@ -56,6 +57,7 @@ * */ @RabbitAvailable(management = true) +@Disabled("Temporary until SF uses Micrometer snaps") public class RabbitRestApiTests { private final CachingConnectionFactory connectionFactory = new CachingConnectionFactory("localhost"); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderIntegrationTests.java index 8dc4affd7a..e3e3ba7477 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderIntegrationTests.java @@ -29,6 +29,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.slf4j.LoggerFactory; import org.slf4j.MDC; @@ -57,6 +58,7 @@ @SpringJUnitConfig(classes = AmqpAppenderConfiguration.class) @DirtiesContext @RabbitAvailable +@Disabled("Temporary") public class AmqpAppenderIntegrationTests { /* logback will automatically find lockback-test.xml */ diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderTests.java index ede1240040..38bf6f5bfe 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderTests.java @@ -30,6 +30,7 @@ import java.net.URI; import java.net.URISyntaxException; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -50,6 +51,7 @@ * * @since 2.0 */ +@Disabled("Temporary") public class AmqpAppenderTests { @Test diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java index 43f4de1136..41c7facb86 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.EnableRabbit; @@ -69,18 +70,28 @@ public SampleTestRunnerConsumer yourCode() { SpansAssert.assertThat(finishedSpans) .haveSameTraceId() .hasSize(4); - SpanAssert.assertThat(finishedSpans.get(0)) - .hasKindEqualTo(Kind.PRODUCER) + List producerSpans = finishedSpans.stream() + .filter(span -> span.getKind().equals(Kind.PRODUCER)) + .collect(Collectors.toList()); + List consumerSpans = finishedSpans.stream() + .filter(span -> span.getKind().equals(Kind.CONSUMER)) + .collect(Collectors.toList()); + SpanAssert.assertThat(producerSpans.get(0)) .hasTag("spring.rabbit.template.name", "template"); - SpanAssert.assertThat(finishedSpans.get(1)) - .hasKindEqualTo(Kind.PRODUCER) + SpanAssert.assertThat(producerSpans.get(0)) + .hasRemoteServiceNameEqualTo("RabbitMQ"); + SpanAssert.assertThat(producerSpans.get(1)) .hasTag("spring.rabbit.template.name", "template"); - SpanAssert.assertThat(finishedSpans.get(2)) - .hasKindEqualTo(Kind.CONSUMER) - .hasTag("spring.rabbit.listener.id", "obs1"); - SpanAssert.assertThat(finishedSpans.get(3)) - .hasKindEqualTo(Kind.CONSUMER) - .hasTag("spring.rabbit.listener.id", "obs2"); + SpanAssert.assertThat(consumerSpans.get(0)) + .hasTagWithKey("spring.rabbit.listener.id"); + SpanAssert.assertThat(consumerSpans.get(0)) + .hasRemoteServiceNameEqualTo("RabbitMQ"); + assertThat(consumerSpans.get(0).getTags().get("spring.rabbit.listener.id")).isIn("obs1", "obs2"); + SpanAssert.assertThat(consumerSpans.get(1)) + .hasTagWithKey("spring.rabbit.listener.id"); + assertThat(consumerSpans.get(1).getTags().get("spring.rabbit.listener.id")).isIn("obs1", "obs2"); + assertThat(consumerSpans.get(0).getTags().get("spring.rabbit.listener.id")) + .isNotEqualTo(consumerSpans.get(1).getTags().get("spring.rabbit.listener.id")); MeterRegistryAssert.assertThat(getMeterRegistry()) .hasTimerWithNameAndTags("spring.rabbit.template", diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java index 58e3d911b3..e1787df31f 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java @@ -38,6 +38,8 @@ import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry; +import org.springframework.amqp.rabbit.support.micrometer.RabbitListenerObservation.DefaultRabbitListenerObservationConvention; +import org.springframework.amqp.rabbit.support.micrometer.RabbitTemplateObservation.DefaultRabbitTemplateObservationConvention; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -84,7 +86,7 @@ void endToEnd(@Autowired Listener listener, @Autowired RabbitTemplate template, .hasFieldOrPropertyWithValue("foo", "some foo value") .hasFieldOrPropertyWithValue("bar", "some bar value"); Deque spans = tracer.getSpans(); - assertThat(spans).hasSize(4); + await().until(() -> spans.size() == 4); SimpleSpan span = spans.poll(); assertThat(span.getTags()).containsEntry("spring.rabbit.template.name", "template"); assertThat(span.getName()).isEqualTo("/observation.testQ1 send"); diff --git a/src/reference/asciidoc/_conventions.adoc b/src/reference/asciidoc/_conventions.adoc new file mode 100644 index 0000000000..76463a0b87 --- /dev/null +++ b/src/reference/asciidoc/_conventions.adoc @@ -0,0 +1,11 @@ +[[observability-conventions]] +=== Observability - Conventions + +Below you can find a list of all `GlobalObservabilityConventions` and `ObservabilityConventions` declared by this project. + +.ObservationConvention implementations +|=== +|ObservationConvention Class Name | Applicable ObservationContext Class Name +|`DefaultRabbitListenerObservationConvention`|`RabbitMessageReceiverContext` +|`DefaultRabbitTemplateObservationConvention`|`RabbitMessageSenderContext` +|=== diff --git a/src/reference/asciidoc/_metrics.adoc b/src/reference/asciidoc/_metrics.adoc new file mode 100644 index 0000000000..328bcf365a --- /dev/null +++ b/src/reference/asciidoc/_metrics.adoc @@ -0,0 +1,44 @@ +[[observability-metrics]] +=== Observability - Metrics + +Below you can find a list of all samples declared by this project. + +[[observability-metrics-listener-observation]] +==== Listener Observation + +____ +Observation for Rabbit listeners. +____ + +**Metric name** `spring.rabbit.listener` (defined by convention class `RabbitListenerObservation$DefaultRabbitListenerObservationConvention`). **Type** `timer` and **base unit** `seconds`. + +Name of the enclosing class `RabbitListenerObservation`. + +IMPORTANT: All tags must be prefixed with `spring.rabbit.listener` prefix! + +.Low cardinality Keys +[cols="a,a"] +|=== +|Name | Description +|`spring.rabbit.listener.id`|Listener id. +|=== + +[[observability-metrics-template-observation]] +==== Template Observation + +____ +Observation for `RabbitTemplate` s. +____ + +**Metric name** `spring.rabbit.template` (defined by convention class `RabbitTemplateObservation$DefaultRabbitTemplateObservationConvention`). **Type** `timer` and **base unit** `seconds`. + +Name of the enclosing class `RabbitTemplateObservation`. + +IMPORTANT: All tags must be prefixed with `spring.rabbit.template` prefix! + +.Low cardinality Keys +[cols="a,a"] +|=== +|Name | Description +|`spring.rabbit.template.name`|Bean name of the template. +|=== diff --git a/src/reference/asciidoc/_spans.adoc b/src/reference/asciidoc/_spans.adoc new file mode 100644 index 0000000000..f816415060 --- /dev/null +++ b/src/reference/asciidoc/_spans.adoc @@ -0,0 +1,38 @@ +[[observability-spans]] +=== Observability - Spans + +Below you can find a list of all spans declared by this project. + +[[observability-spans-listener-observation]] +==== Listener Observation Span + +> Observation for Rabbit listeners. + +**Span name** `spring.rabbit.listener` (defined by convention class `RabbitListenerObservation$DefaultRabbitListenerObservationConvention`). + +Name of the enclosing class `RabbitListenerObservation`. + +IMPORTANT: All tags and event names must be prefixed with `spring.rabbit.listener` prefix! + +.Tag Keys +|=== +|Name | Description +|`spring.rabbit.listener.id`|Listener id. +|=== + +[[observability-spans-template-observation]] +==== Template Observation Span + +> Observation for `RabbitTemplate` s. + +**Span name** `spring.rabbit.template` (defined by convention class `RabbitTemplateObservation$DefaultRabbitTemplateObservationConvention`). + +Name of the enclosing class `RabbitTemplateObservation`. + +IMPORTANT: All tags and event names must be prefixed with `spring.rabbit.template` prefix! + +.Tag Keys +|=== +|Name | Description +|`spring.rabbit.template.name`|Bean name of the template. +|=== diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index 0c83b0a3b6..910f41b8d9 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -3764,9 +3764,9 @@ The timers are named `spring.rabbitmq.listener` and have the following tags: You can add additional tags using the `micrometerTags` container property. -Also see <>. +Also see <>. -[[observation]] +[[micrometer-observation]] ===== Micrometer Observation Using Micrometer for observation is now supported, since version 3.0, for the `RabbitTemplate` and listener containers. @@ -3781,6 +3781,8 @@ The default implementations add the `bean.name` tag for template observations an You can either subclass `DefaultRabbitTemplateObservationConvention` or `DefaultRabbitListenerObservationConvention` or provide completely new implementations. +See <> for more details. + [[containers-and-broker-named-queues]] ==== Containers and Broker-Named queues diff --git a/src/reference/asciidoc/appendix.adoc b/src/reference/asciidoc/appendix.adoc index b78fc1da3e..294ffb4c5e 100644 --- a/src/reference/asciidoc/appendix.adoc +++ b/src/reference/asciidoc/appendix.adoc @@ -1,3 +1,14 @@ +[appendix] +[[observation-gen]] +== Micrometer Observation Documentation + +include::_metrics.adoc[] + +include::_spans.adoc[] + +include::_conventions.adoc[] + +[appendix] [[change-history]] == Change History diff --git a/src/reference/asciidoc/index.adoc b/src/reference/asciidoc/index.adoc index 2ca7f6a9d4..71ff75d97b 100644 --- a/src/reference/asciidoc/index.adoc +++ b/src/reference/asciidoc/index.adoc @@ -67,5 +67,4 @@ In addition to this reference documentation, there exist a number of other resou include::further-reading.adoc[] -[appendix] include::appendix.adoc[] From c143c5b02233c062e51272b4cf98306bcf42cdac Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 11 Oct 2022 11:09:51 -0400 Subject: [PATCH 165/737] GH-1419: Remove RabbitMQ http-client Usage Resolves https://github.com/spring-projects/spring-amqp/issues/1419 Use Spring WebFlux instead, while allowing the user to choose some other technology in the `LocalizedQueueConnectionFactory`. * Rename DefaultNodeLocator; add generics. * Remove unnecessary dependencies. --- build.gradle | 16 +- .../rabbit/junit/BrokerRunningSupport.java | 33 ++- .../rabbit/junit/RabbitAvailableTests.java | 4 +- .../stream/listener/RabbitListenerTests.java | 47 +++- .../LocalizedQueueConnectionFactory.java | 178 +++++++++---- .../rabbit/connection/WebFluxNodeLocator.java | 70 ++++++ .../EnableRabbitIntegrationTests.java | 14 +- ...ueueConnectionFactoryIntegrationTests.java | 19 +- .../LocalizedQueueConnectionFactoryTests.java | 52 ++-- .../core/FixedReplyQueueDeadLetterTests.java | 50 +--- .../rabbit/core/NeedsManagementTests.java | 96 ++++++++ .../core/RabbitAdminIntegrationTests.java | 25 +- .../amqp/rabbit/core/RabbitAdminTests.java | 15 +- .../amqp/rabbit/core/RabbitRestApiTests.java | 233 ------------------ src/reference/asciidoc/amqp.adoc | 42 ++++ src/reference/asciidoc/whats-new.adoc | 3 + 16 files changed, 498 insertions(+), 399 deletions(-) create mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/WebFluxNodeLocator.java create mode 100644 spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/NeedsManagementTests.java delete mode 100644 spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitRestApiTests.java diff --git a/build.gradle b/build.gradle index e1874a35a6..4ec14970d0 100644 --- a/build.gradle +++ b/build.gradle @@ -44,7 +44,6 @@ ext { assertkVersion = '0.24' awaitilityVersion = '4.2.0' commonsCompressVersion = '1.20' - commonsHttpClientVersion = '4.5.13' commonsPoolVersion = '2.11.1' googleJsr305Version = '3.0.2' hamcrestVersion = '2.2' @@ -62,7 +61,6 @@ ext { mockitoVersion = '4.8.0' rabbitmqStreamVersion = '0.8.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.16.0' - rabbitmqHttpClientVersion = '3.12.1' reactorVersion = '2022.0.0-SNAPSHOT' snappyVersion = '1.1.8.4' springDataVersion = '2022.0.0-SNAPSHOT' @@ -384,11 +382,11 @@ project('spring-rabbit') { api project(':spring-amqp') api "com.rabbitmq:amqp-client:$rabbitmqVersion" - optionalApi "com.rabbitmq:http-client:$rabbitmqHttpClientVersion" optionalApi 'org.springframework:spring-aop' api 'org.springframework:spring-context' api 'org.springframework:spring-messaging' api 'org.springframework:spring-tx' + optionalApi 'org.springframework:spring-webflux' optionalApi 'io.projectreactor:reactor-core' optionalApi "ch.qos.logback:logback-classic:$logbackVersion" optionalApi 'org.apache.logging.log4j:log4j-core' @@ -409,8 +407,6 @@ project('spring-rabbit') { testImplementation 'io.micrometer:micrometer-tracing-bridge-brave' testImplementation 'io.micrometer:micrometer-tracing-test' testImplementation 'io.micrometer:micrometer-tracing-integration-test' - testRuntimeOnly 'org.springframework:spring-web' - testRuntimeOnly "org.apache.httpcomponents:httpclient:$commonsHttpClientVersion" testRuntimeOnly 'com.fasterxml.jackson.core:jackson-core' testRuntimeOnly 'com.fasterxml.jackson.core:jackson-databind' testRuntimeOnly 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' @@ -465,14 +461,12 @@ project('spring-rabbit-stream') { api project(':spring-rabbit') api "com.rabbitmq:stream-client:$rabbitmqStreamVersion" - optionalApi "com.rabbitmq:http-client:$rabbitmqHttpClientVersion" testApi project(':spring-rabbit-junit') testRuntimeOnly 'com.fasterxml.jackson.core:jackson-core' testRuntimeOnly 'com.fasterxml.jackson.core:jackson-databind' testRuntimeOnly 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' testRuntimeOnly 'com.fasterxml.jackson.module:jackson-module-kotlin' - testRuntimeOnly "org.apache.httpcomponents:httpclient:$commonsHttpClientVersion" testRuntimeOnly "org.apache.commons:commons-compress:$commonsCompressVersion" testRuntimeOnly "org.xerial.snappy:snappy-java:$snappyVersion" testRuntimeOnly "org.lz4:lz4-java:$lz4Version" @@ -494,16 +488,14 @@ project('spring-rabbit-junit') { exclude group: 'org.hamcrest', module: 'hamcrest-core' } api "com.rabbitmq:amqp-client:$rabbitmqVersion" - api ("com.rabbitmq:http-client:$rabbitmqHttpClientVersion") { - exclude group: 'org.springframework', module: 'spring-web' - } - api 'org.springframework:spring-web' + api 'org.springframework:spring-webflux' api 'org.junit.jupiter:junit-jupiter-api' api "org.assertj:assertj-core:$assertjVersion" optionalApi "ch.qos.logback:logback-classic:$logbackVersion" optionalApi 'org.apache.logging.log4j:log4j-core' compileOnly 'org.apiguardian:apiguardian-api:1.0.0' - + testRuntimeOnly 'com.fasterxml.jackson.core:jackson-core' + testRuntimeOnly 'com.fasterxml.jackson.core:jackson-databind' } } diff --git a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java index 2852ef2857..90b2d9f7e3 100644 --- a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java +++ b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -17,8 +17,11 @@ package org.springframework.amqp.rabbit.junit; import java.io.IOException; +import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -30,13 +33,17 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.MediaType; import org.springframework.util.Base64Utils; import org.springframework.util.StringUtils; +import org.springframework.web.reactive.function.client.ExchangeFilterFunctions; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.UriUtils; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; -import com.rabbitmq.http.client.Client; /** * A class that can be used to prevent integration tests from failing if the Rabbit broker application is @@ -372,8 +379,7 @@ private Channel createQueues(Connection connection) throws IOException, URISynta } } if (this.management) { - Client client = new Client(getAdminUri(), this.adminUser, this.adminPassword); - if (!client.alivenessTest("/")) { + if (!alivenessTest()) { throw new BrokerNotAliveException("Aliveness test failed for localhost:15672 guest/quest; " + "management not available"); } @@ -381,6 +387,25 @@ private Channel createQueues(Connection connection) throws IOException, URISynta return channel; } + private boolean alivenessTest() throws URISyntaxException { + WebClient client = WebClient.builder() + .filter(ExchangeFilterFunctions.basicAuthentication(this.adminUser, this.adminPassword)) + .build(); + URI uri = new URI(getAdminUri()) + .resolve("/api/aliveness-test/" + UriUtils.encodePathSegment("/", StandardCharsets.UTF_8)); + HashMap result = client.get() + .uri(uri) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + }) + .block(Duration.ofSeconds(10)); // NOSONAR magic# + if (result != null) { + return result.get("status").equals("ok"); + } + return false; + } + public static boolean fatal() { String serversRequired = System.getenv(BROKER_REQUIRED); if (Boolean.parseBoolean(serversRequired)) { diff --git a/spring-rabbit-junit/src/test/java/org/springframework/amqp/rabbit/junit/RabbitAvailableTests.java b/spring-rabbit-junit/src/test/java/org/springframework/amqp/rabbit/junit/RabbitAvailableTests.java index 9c93f81d3f..d23b0190a5 100644 --- a/spring-rabbit-junit/src/test/java/org/springframework/amqp/rabbit/junit/RabbitAvailableTests.java +++ b/spring-rabbit-junit/src/test/java/org/springframework/amqp/rabbit/junit/RabbitAvailableTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 the original author or authors. + * Copyright 2017-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. @@ -30,7 +30,7 @@ * @since 2.0.2 * */ -@RabbitAvailable(queues = "rabbitAvailableTests.queue") +@RabbitAvailable(queues = "rabbitAvailableTests.queue", management = true) public class RabbitAvailableTests { @Test diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java index fa5f47f82c..ba4bd3e534 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java @@ -18,14 +18,18 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Queue; @@ -42,6 +46,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.MediaType; import org.springframework.rabbit.stream.config.StreamRabbitListenerContainerFactory; import org.springframework.rabbit.stream.producer.RabbitStreamTemplate; import org.springframework.rabbit.stream.retry.StreamRetryOperationsInterceptorFactoryBean; @@ -50,9 +56,10 @@ import org.springframework.retry.interceptor.RetryOperationsInterceptor; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.web.reactive.function.client.ExchangeFilterFunctions; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.UriUtils; -import com.rabbitmq.http.client.Client; -import com.rabbitmq.http.client.domain.QueueInfo; import com.rabbitmq.stream.Address; import com.rabbitmq.stream.Environment; import com.rabbitmq.stream.Message; @@ -99,12 +106,38 @@ void nativeMsg(@Autowired RabbitTemplate template) throws InterruptedException { assertThat(this.config.latch4.await(10, TimeUnit.SECONDS)).isTrue(); } + @SuppressWarnings("unchecked") @Test - @Disabled("Temporary until SF uses Micrometer snaps") void queueOverAmqp() throws Exception { - Client client = new Client("http://guest:guest@localhost:" + managementPort() + "/api"); - QueueInfo queue = client.getQueue("/", "stream.created.over.amqp"); - assertThat(queue.getArguments().get("x-queue-type")).isEqualTo("stream"); + WebClient client = WebClient.builder() + .filter(ExchangeFilterFunctions.basicAuthentication("guest", "guest")) + .build(); + Map queue = queueInfo("stream.created.over.amqp"); + assertThat(((Map) queue.get("arguments")).get("x-queue-type")).isEqualTo("stream"); + } + + private Map queueInfo(String queueName) throws URISyntaxException { + WebClient client = createClient("guest", "guest"); + URI uri = queueUri(queueName); + return client.get() + .uri(uri) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + }) + .block(Duration.ofSeconds(10)); + } + + private URI queueUri(String queue) throws URISyntaxException { + URI uri = new URI("http://localhost:" + managementPort() + "/api") + .resolve("/api/queues/" + UriUtils.encodePathSegment("/", StandardCharsets.UTF_8) + "/" + queue); + return uri; + } + + private WebClient createClient(String adminUser, String adminPassword) { + return WebClient.builder() + .filter(ExchangeFilterFunctions.basicAuthentication(adminUser, adminPassword)) + .build(); } @Configuration(proxyBeanMethods = false) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java index e209734ec6..decb22fa1c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java @@ -16,7 +16,7 @@ package org.springframework.amqp.rabbit.connection; -import java.net.MalformedURLException; +import java.net.URI; import java.net.URISyntaxException; import java.util.AbstractMap.SimpleImmutableEntry; import java.util.Arrays; @@ -32,12 +32,10 @@ import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.beans.factory.DisposableBean; import org.springframework.core.io.Resource; +import org.springframework.core.log.LogAccessor; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import com.rabbitmq.http.client.Client; -import com.rabbitmq.http.client.domain.QueueInfo; - /** * A {@link RoutingConnectionFactory} that determines the node on which a queue is located and * returns a factory that connects directly to that node. @@ -84,6 +82,8 @@ public class LocalizedQueueConnectionFactory implements ConnectionFactory, Routi private final String trustStorePassPhrase; + private NodeLocator nodeLocator = new WebFluxNodeLocator(); + /** * @param defaultConnectionFactory the fallback connection factory to use if the queue * can't be located. @@ -200,6 +200,15 @@ private static Map nodesAddressesToMap(String[] nodes, String[] .collect(Collectors.toMap(SimpleImmutableEntry::getKey, SimpleImmutableEntry::getValue)); } + /** + * Set a {@link NodeLocator} to use to find the node address for the leader. + * @param nodeLocator the locator. + * @since 2.4.8 + */ + public void setNodeLocator(NodeLocator nodeLocator) { + this.nodeLocator = nodeLocator; + } + @Override public Connection createConnection() throws AmqpException { return this.defaultConnectionFactory.createConnection(); @@ -244,7 +253,8 @@ public void clearConnectionListeners() { public ConnectionFactory getTargetConnectionFactory(Object key) { String queue = ((String) key); queue = queue.substring(1, queue.length() - 1); - Assert.isTrue(!queue.contains(","), () -> "Cannot use LocalizedQueueConnectionFactory with more than one queue: " + key); + Assert.isTrue(!queue.contains(","), + () -> "Cannot use LocalizedQueueConnectionFactory with more than one queue: " + key); ConnectionFactory connectionFactory = determineConnectionFactory(queue); if (connectionFactory == null) { return this.defaultConnectionFactory; @@ -256,51 +266,12 @@ public ConnectionFactory getTargetConnectionFactory(Object key) { @Nullable private ConnectionFactory determineConnectionFactory(String queue) { - for (int i = 0; i < this.adminUris.length; i++) { - String adminUri = this.adminUris[i]; - if (!adminUri.endsWith("/api/")) { - adminUri += "/api/"; - } - try { - Client client = createClient(adminUri, this.username, this.password); - QueueInfo queueInfo = client.getQueue(this.vhost, queue); - if (queueInfo != null) { - String node = queueInfo.getNode(); - if (node != null) { - String uri = this.nodeToAddress.get(node); - if (uri != null) { - return nodeConnectionFactory(queue, node, uri); - } - if (this.logger.isDebugEnabled()) { - this.logger.debug("No match for node: " + node); - } - } - } - else { - throw new AmqpException("Admin returned null QueueInfo"); - } - } - catch (Exception e) { - this.logger.warn("Failed to determine queue location for: " + queue + " at: " + - adminUri + ": " + e.getMessage()); - } + ConnectionFactory cf = this.nodeLocator.locate(this.adminUris, this.nodeToAddress, this.vhost, this.username, + this.password, queue, this::nodeConnectionFactory); + if (cf == null) { + this.logger.warn("Failed to determine queue location for: " + queue + ", using default connection factory"); } - this.logger.warn("Failed to determine queue location for: " + queue + ", using default connection factory"); - return null; - } - - /** - * Create a client instance. - * @param adminUri the admin URI. - * @param username the username - * @param password the password. - * @return The client. - * @throws MalformedURLException if the URL is malformed - * @throws URISyntaxException if there is a syntax error. - */ - protected Client createClient(String adminUri, String username, String password) throws MalformedURLException, - URISyntaxException { - return new Client(adminUri, username, password); + return cf; } private synchronized ConnectionFactory nodeConnectionFactory(String queue, String node, String address) { @@ -372,4 +343,113 @@ public void destroy() { resetConnection(); } + /** + * Used to obtain a connection factory for the queue leader. + * + * @param the client type. + * @since 2.4.8 + */ + public interface NodeLocator { + + LogAccessor LOGGER = new LogAccessor(LogFactory.getLog(NodeLocator.class)); + + /** + * Return a connection factory for the leader node for the queue. + * @param adminUris an array of admin URIs. + * @param nodeToAddress a map of node names to node addresses (AMQP). + * @param vhost the vhost. + * @param username the user name. + * @param password the password. + * @param queue the queue name. + * @param factoryFunction an internal function to find or create the factory. + * @return a connection factory, if the leader node was found; null otherwise. + */ + @Nullable + default ConnectionFactory locate(String[] adminUris, Map nodeToAddress, String vhost, + String username, String password, String queue, FactoryFinder factoryFunction) { + + T client = createClient(username, password); + + for (int i = 0; i < adminUris.length; i++) { + String adminUri = adminUris[i]; + if (!adminUri.endsWith("/api/")) { + adminUri += "/api/"; + } + try { + String uri = new URI(adminUri) + .resolve("/api/queues/").toString(); + HashMap queueInfo = restCall(client, uri, vhost, queue); + if (queueInfo != null) { + String node = (String) queueInfo.get("node"); + if (node != null) { + String nodeUri = nodeToAddress.get(node); + if (uri != null) { + close(client); + return factoryFunction.locate(queue, node, nodeUri); + } + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("No match for node: " + node); + } + } + } + else { + throw new AmqpException("Admin returned null QueueInfo"); + } + } + catch (Exception e) { + LOGGER.warn("Failed to determine queue location for: " + queue + " at: " + + adminUri + ": " + e.getMessage()); + } + } + LOGGER.warn("Failed to determine queue location for: " + queue + ", using default connection factory"); + close(client); + return null; + } + + /** + * Create a client for subsequent use. + * @param userName the user name. + * @param password the password. + * @return the client. + */ + T createClient(String userName, String password); + + /** + * Close the client. + * @param client the client. + */ + default void close(T client) { + } + + /** + * Retrieve a map of queue properties using the RabbitMQ Management REST API. + * @param baseUri the base uri. + * @param vhost the virtual host. + * @param queue the queue name. + * @return the map of queue properties. + * @throws URISyntaxException if the syntax is bad. + */ + HashMap restCall(T client, String baseUri, String vhost, String queue) + throws URISyntaxException; + + } + + /** + * Callback to determine the connection factory using the provided information. + * @since 2.4.8 + */ + @FunctionalInterface + public interface FactoryFinder { + + /** + * Locate or create a factory. + * @param queueName the queue name. + * @param node the node name. + * @param nodeUri the node URI. + * @return the factory. + */ + ConnectionFactory locate(String queueName, String node, String nodeUri); + + } + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/WebFluxNodeLocator.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/WebFluxNodeLocator.java new file mode 100644 index 0000000000..bedf67f017 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/WebFluxNodeLocator.java @@ -0,0 +1,70 @@ +/* + * 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.connection; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.HashMap; + +import org.springframework.amqp.rabbit.connection.LocalizedQueueConnectionFactory.NodeLocator; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.client.ExchangeFilterFunctions; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.UriUtils; + +/** + * A {@link NodeLocator} using the Spring WebFlux {@link WebClient}. + * + * @author Gary Russell + * @since 2.4.8 + * + */ +public class WebFluxNodeLocator implements NodeLocator { + + @Override + public HashMap restCall(WebClient client, String baseUri, String vhost, String queue) + throws URISyntaxException { + + URI uri = new URI(baseUri) + .resolve("/api/queues/" + UriUtils.encodePathSegment(vhost, StandardCharsets.UTF_8) + "/" + queue); + HashMap queueInfo = client.get() + .uri(uri) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + }) + .block(Duration.ofSeconds(10)); // NOSONAR magic# + return queueInfo; + } + + /** + * Create a client instance. + * @param username the username + * @param password the password. + * @return The client. + */ + @Override + public WebClient createClient(String username, String password) { + return WebClient.builder() + .filter(ExchangeFilterFunctions.basicAuthentication(username, password)) + .build(); + } + +} diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java index 45505f86be..6eec2a3162 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java @@ -47,7 +47,6 @@ import org.aopalliance.intercept.MethodInvocation; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -68,6 +67,7 @@ import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionNameStrategy; import org.springframework.amqp.rabbit.connection.SimplePropertyValueConnectionNameStrategy; +import org.springframework.amqp.rabbit.core.NeedsManagementTests; import org.springframework.amqp.rabbit.core.RabbitAdmin; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.junit.BrokerRunningSupport; @@ -143,8 +143,6 @@ import org.springframework.validation.annotation.Validated; import com.rabbitmq.client.Channel; -import com.rabbitmq.http.client.Client; -import com.rabbitmq.http.client.domain.QueueInfo; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; @@ -180,7 +178,7 @@ "test.custom.argument", "test.arg.validation", "manual.acks.1", "manual.acks.2", "erit.batch.1", "erit.batch.2", "erit.batch.3", "erit.mp.arg" }, purgeAfterEach = false) -public class EnableRabbitIntegrationTests { +public class EnableRabbitIntegrationTests extends NeedsManagementTests { @Autowired private RabbitTemplate rabbitTemplate; @@ -833,16 +831,14 @@ public void testHeadersExchange() throws Exception { } @Test - @Disabled("Temporary until SF uses Micrometer snaps") public void deadLetterOnDefaultExchange() { this.rabbitTemplate.convertAndSend("amqp656", "foo"); assertThat(this.rabbitTemplate.receiveAndConvert("amqp656dlq", 10000)).isEqualTo("foo"); try { - Client rabbitRestClient = new Client("http://localhost:15672/api/", "guest", "guest"); - QueueInfo amqp656 = rabbitRestClient.getQueue("/", "amqp656"); + Map amqp656 = await().until(() -> queueInfo("amqp656"), q -> q != null); if (amqp656 != null) { - assertThat(amqp656.getArguments().get("test-empty")).isEqualTo(""); - assertThat(amqp656.getArguments().get("test-null")).isEqualTo("undefined"); + assertThat(arguments(amqp656).get("test-empty")).isEqualTo(""); + assertThat(arguments(amqp656).get("test-null")).isEqualTo("undefined"); } } catch (Exception e) { diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryIntegrationTests.java index 24f1deb319..9a20edf235 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryIntegrationTests.java @@ -17,12 +17,13 @@ package org.springframework.amqp.rabbit.connection; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import java.util.Map; import java.util.UUID; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Queue; @@ -35,8 +36,7 @@ * * @author Gary Russell */ -@RabbitAvailable(management = true) -@Disabled("Temporary until SF uses Micrometer snaps") +@RabbitAvailable(management = true, queues = "local") public class LocalizedQueueConnectionFactoryIntegrationTests { private LocalizedQueueConnectionFactory lqcf; @@ -63,7 +63,6 @@ public void tearDown() { } @Test - @Disabled("Temporary until SF uses Micrometer snaps") public void testConnect() throws Exception { RabbitAdmin admin = new RabbitAdmin(this.lqcf); Queue queue = new Queue(UUID.randomUUID().toString(), false, false, true); @@ -75,4 +74,16 @@ public void testConnect() throws Exception { admin.deleteQueue(queue.getName()); } + @Test + void findLocal() { + ConnectionFactory defaultCf = mock(ConnectionFactory.class); + LocalizedQueueConnectionFactory lqcf = new LocalizedQueueConnectionFactory(defaultCf, + Map.of("rabbit@localhost", "localhost:5672"), new String[] { "http://localhost:15672" }, + "/", "guest", "guest", false, null); + ConnectionFactory cf = lqcf.getTargetConnectionFactory("[local]"); + RabbitAdmin admin = new RabbitAdmin(cf); + assertThat(admin.getQueueProperties("local")).isNotNull(); + lqcf.destroy(); + } + } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryTests.java index e7b55e55b5..9dc9246b20 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2020 the original author or authors. + * Copyright 2015-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. @@ -45,11 +45,16 @@ import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; import org.springframework.amqp.utils.test.TestUtils; import org.springframework.beans.DirectFieldAccessor; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.test.web.reactive.server.HttpHandlerConnector; +import org.springframework.web.reactive.function.client.WebClient; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Consumer; -import com.rabbitmq.http.client.Client; -import com.rabbitmq.http.client.domain.QueueInfo; +import reactor.core.publisher.Mono; /** @@ -76,8 +81,8 @@ public void testFailOver() throws Exception { String username = "guest"; String password = "guest"; final AtomicBoolean firstServer = new AtomicBoolean(true); - final Client client1 = doCreateClient(adminUris[0], username, password, nodes[0]); - final Client client2 = doCreateClient(adminUris[1], username, password, nodes[1]); + final WebClient client1 = doCreateClient(adminUris[0], username, password, nodes[0]); + final WebClient client2 = doCreateClient(adminUris[1], username, password, nodes[1]); final Map mockCFs = new HashMap(); CountDownLatch latch1 = new CountDownLatch(1); CountDownLatch latch2 = new CountDownLatch(1); @@ -86,17 +91,20 @@ public void testFailOver() throws Exception { LocalizedQueueConnectionFactory lqcf = new LocalizedQueueConnectionFactory(defaultConnectionFactory, addresses, adminUris, nodes, vhost, username, password, false, null) { - @Override - protected Client createClient(String adminUri, String username, String password) { - return firstServer.get() ? client1 : client2; - } - @Override protected ConnectionFactory createConnectionFactory(String address, String node) { return mockCFs.get(address); } }; + lqcf.setNodeLocator(new WebFluxNodeLocator() { + + @Override + public WebClient createClient(String username, String password) { + return firstServer.get() ? client1 : client2; + } + + }); Map nodeAddress = TestUtils.getPropertyValue(lqcf, "nodeToAddress", Map.class); assertThat(nodeAddress.get("rabbit@foo")).isEqualTo(rabbit1); assertThat(nodeAddress.get("rabbit@bar")).isEqualTo(rabbit2); @@ -144,12 +152,20 @@ private boolean assertLog(List logRows, String expected) { return false; } - private Client doCreateClient(String uri, String username, String password, String node) { - Client client = mock(Client.class); - QueueInfo queueInfo = new QueueInfo(); - queueInfo.setNode(node); - given(client.getQueue("/", "q")).willReturn(queueInfo); - return client; + private WebClient doCreateClient(String uri, String username, String password, String node) { + ClientHttpConnector httpConnector = + new HttpHandlerConnector((request, response) -> { + response.setStatusCode(HttpStatus.OK); + response.getHeaders().setContentType(MediaType.APPLICATION_JSON); + Mono json = Mono + .just(response.bufferFactory().wrap(("{\"node\":\"" + node + "\"}").getBytes())); + return response.writeWith(json) + .then(Mono.defer(response::setComplete)); + }); + + return WebClient.builder() + .clientConnector(httpConnector) + .build(); } @Test @@ -182,8 +198,8 @@ private ConnectionFactory mockCF(final String address, final CountDownLatch latc given(channel.isOpen()).willReturn(true, false); willAnswer(invocation -> { String tag = UUID.randomUUID().toString(); - consumers.put(address, invocation.getArgument(6)); - consumerTags.put(address, tag); + this.consumers.put(address, invocation.getArgument(6)); + this.consumerTags.put(address, tag); if (latch != null) { latch.countDown(); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/FixedReplyQueueDeadLetterTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/FixedReplyQueueDeadLetterTests.java index f7e862321d..f4dbce98ad 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/FixedReplyQueueDeadLetterTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/FixedReplyQueueDeadLetterTests.java @@ -26,8 +26,6 @@ import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Binding; @@ -40,9 +38,7 @@ import org.springframework.amqp.core.QueueBuilder.Overflow; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; -import org.springframework.amqp.rabbit.junit.BrokerRunningSupport; import org.springframework.amqp.rabbit.junit.RabbitAvailable; -import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter; import org.springframework.beans.factory.annotation.Autowired; @@ -51,10 +47,6 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import com.rabbitmq.http.client.Client; -import com.rabbitmq.http.client.domain.ExchangeInfo; -import com.rabbitmq.http.client.domain.QueueInfo; - /** * * @author Gary Russell @@ -64,10 +56,7 @@ @SpringJUnitConfig @DirtiesContext @RabbitAvailable(management = true) -@Disabled("Temporary until SF uses Micrometer snaps") -public class FixedReplyQueueDeadLetterTests { - - private static BrokerRunningSupport brokerRunning; +public class FixedReplyQueueDeadLetterTests extends NeedsManagementTests { @Autowired private RabbitTemplate rabbitTemplate; @@ -75,11 +64,6 @@ public class FixedReplyQueueDeadLetterTests { @Autowired private DeadListener deadListener; - @BeforeAll - static void setUp() { - brokerRunning = RabbitAvailableCondition.getBrokerRunning(); - } - @AfterAll static void tearDown() { brokerRunning.deleteQueues("all.args.1", "all.args.2", "all.args.3", "test.quorum"); @@ -100,10 +84,8 @@ void test() throws Exception { @Test void testQueueArgs1() throws MalformedURLException, URISyntaxException, InterruptedException { - Client client = new Client(brokerRunning.getAdminUri(), brokerRunning.getAdminUser(), - brokerRunning.getAdminPassword()); - QueueInfo queue = await().until(() -> client.getQueue("/", "all.args.1"), que -> que != null); - Map arguments = queue.getArguments(); + Map queue = await().until(() -> queueInfo("all.args.1"), que -> que != null); + Map arguments = arguments(queue); assertThat(arguments.get("x-message-ttl")).isEqualTo(1000); assertThat(arguments.get("x-expires")).isEqualTo(200_000); assertThat(arguments.get("x-max-length")).isEqualTo(42); @@ -119,10 +101,8 @@ void testQueueArgs1() throws MalformedURLException, URISyntaxException, Interrup @Test void testQueueArgs2() throws MalformedURLException, URISyntaxException, InterruptedException { - Client client = new Client(brokerRunning.getAdminUri(), brokerRunning.getAdminUser(), - brokerRunning.getAdminPassword()); - QueueInfo queue = await().until(() -> client.getQueue("/", "all.args.2"), que -> que != null); - Map arguments = queue.getArguments(); + Map queue = await().until(() -> queueInfo("all.args.2"), que -> que != null); + Map arguments = arguments(queue); assertThat(arguments.get("x-message-ttl")).isEqualTo(1000); assertThat(arguments.get("x-expires")).isEqualTo(200_000); assertThat(arguments.get("x-max-length")).isEqualTo(42); @@ -136,11 +116,9 @@ void testQueueArgs2() throws MalformedURLException, URISyntaxException, Interrup } @Test - void testQueueArgs3() throws MalformedURLException, URISyntaxException, InterruptedException { - Client client = new Client(brokerRunning.getAdminUri(), brokerRunning.getAdminUser(), - brokerRunning.getAdminPassword()); - QueueInfo queue = await().until(() -> client.getQueue("/", "all.args.3"), que -> que != null); - Map arguments = queue.getArguments(); + void testQueueArgs3() throws URISyntaxException { + Map queue = await().until(() -> queueInfo("all.args.3"), que -> que != null); + Map arguments = arguments(queue); assertThat(arguments.get("x-message-ttl")).isEqualTo(1000); assertThat(arguments.get("x-expires")).isEqualTo(200_000); assertThat(arguments.get("x-max-length")).isEqualTo(42); @@ -152,19 +130,17 @@ void testQueueArgs3() throws MalformedURLException, URISyntaxException, Interrup assertThat(arguments.get("x-queue-mode")).isEqualTo("lazy"); assertThat(arguments.get(Queue.X_QUEUE_LEADER_LOCATOR)).isEqualTo(LeaderLocator.random.getValue()); - ExchangeInfo exchange = client.getExchange("/", "dlx.test.requestEx"); - assertThat(exchange.getArguments().get("alternate-exchange")).isEqualTo("alternate"); + Map exchange = exchangeInfo("dlx.test.requestEx"); + assertThat(arguments(exchange).get("alternate-exchange")).isEqualTo("alternate"); } /* * Does not require a 3.8 broker - they are just arbitrary arguments. */ @Test - void testQuorumArgs() throws MalformedURLException, URISyntaxException, InterruptedException { - Client client = new Client(brokerRunning.getAdminUri(), brokerRunning.getAdminUser(), - brokerRunning.getAdminPassword()); - QueueInfo queue = await().until(() -> client.getQueue("/", "test.quorum"), que -> que != null); - Map arguments = queue.getArguments(); + void testQuorumArgs() { + Map queue = await().until(() -> queueInfo("test.quorum"), que -> que != null); + Map arguments = arguments(queue); assertThat(arguments.get("x-queue-type")).isEqualTo("quorum"); assertThat(arguments.get("x-delivery-limit")).isEqualTo(10); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/NeedsManagementTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/NeedsManagementTests.java new file mode 100644 index 0000000000..a854488588 --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/NeedsManagementTests.java @@ -0,0 +1,96 @@ +/* + * 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.core; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Map; + +import org.junit.jupiter.api.BeforeAll; + +import org.springframework.amqp.rabbit.junit.BrokerRunningSupport; +import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.client.ExchangeFilterFunctions; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.UriUtils; + +/** + * @author Gary Russell + * @since 2.4.8 + * + */ +public abstract class NeedsManagementTests { + + protected static BrokerRunningSupport brokerRunning; + + @BeforeAll + static void setUp() { + brokerRunning = RabbitAvailableCondition.getBrokerRunning(); + } + + protected Map queueInfo(String queueName) throws URISyntaxException { + WebClient client = createClient(brokerRunning.getAdminUser(), brokerRunning.getAdminPassword()); + URI uri = queueUri(queueName); + return client.get() + .uri(uri) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + }) + .block(Duration.ofSeconds(10)); + } + + protected Map exchangeInfo(String name) throws URISyntaxException { + WebClient client = createClient(brokerRunning.getAdminUser(), brokerRunning.getAdminPassword()); + URI uri = exchangeUri(name); + return client.get() + .uri(uri) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + }) + .block(Duration.ofSeconds(10)); + } + + @SuppressWarnings("unchecked") + protected Map arguments(Map infoMap) { + return (Map) infoMap.get("arguments"); + } + + private URI queueUri(String queue) throws URISyntaxException { + URI uri = new URI(brokerRunning.getAdminUri()) + .resolve("/api/queues/" + UriUtils.encodePathSegment("/", StandardCharsets.UTF_8) + "/" + queue); + return uri; + } + + private URI exchangeUri(String queue) throws URISyntaxException { + URI uri = new URI(brokerRunning.getAdminUri()) + .resolve("/api/exchanges/" + UriUtils.encodePathSegment("/", StandardCharsets.UTF_8) + "/" + queue); + return uri; + } + + private WebClient createClient(String adminUser, String adminPassword) { + return WebClient.builder() + .filter(ExchangeFilterFunctions.basicAuthentication(adminUser, adminPassword)) + .build(); + } + +} diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminIntegrationTests.java index 85cdc2b05c..abf8f76cb3 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminIntegrationTests.java @@ -22,11 +22,11 @@ import java.io.IOException; import java.time.Duration; +import java.util.Map; import java.util.UUID; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.amqp.AmqpIOException; @@ -52,8 +52,6 @@ import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; -import com.rabbitmq.http.client.Client; -import com.rabbitmq.http.client.domain.ExchangeInfo; /** * @author Dave Syer @@ -63,8 +61,7 @@ * @author Artem Bilan */ @RabbitAvailable(management = true) -@Disabled("Temporary until SF uses Micrometer snaps") -public class RabbitAdminIntegrationTests { +public class RabbitAdminIntegrationTests extends NeedsManagementTests { private final CachingConnectionFactory connectionFactory = new CachingConnectionFactory(); @@ -258,9 +255,9 @@ public void testDeleteExchangeWithInternalOption() throws Exception { exchange.setInternal(true); rabbitAdmin.declareExchange(exchange); - ExchangeInfo exchange2 = getExchange(exchangeName); - assertThat(exchange2.getType()).isEqualTo(ExchangeTypes.DIRECT); - assertThat(exchange2.isInternal()).isTrue(); + Map exchange2 = getExchange(exchangeName); + assertThat(exchange2.get("type")).isEqualTo(ExchangeTypes.DIRECT); + assertThat(exchange2.get("internal")).isEqualTo(Boolean.TRUE); boolean result = rabbitAdmin.deleteExchange(exchangeName); @@ -370,6 +367,7 @@ public void testQueueDeclareBad() { this.rabbitAdmin.deleteQueue(queue.getName()); } + @SuppressWarnings("unchecked") @Test public void testDeclareDelayedExchange() throws Exception { DirectExchange exchange = new DirectExchange("test.delayed.exchange"); @@ -415,18 +413,17 @@ public void testDeclareDelayedExchange() throws Exception { assertThat(received.getMessageProperties().getReceivedDelay()).isEqualTo(Integer.valueOf(1000)); assertThat(System.currentTimeMillis() - t1).isGreaterThan(950L); - ExchangeInfo exchange2 = getExchange(exchangeName); - assertThat(exchange2.getArguments().get("x-delayed-type")).isEqualTo(ExchangeTypes.DIRECT); - assertThat(exchange2.getType()).isEqualTo("x-delayed-message"); + Map exchange2 = getExchange(exchangeName); + assertThat(arguments(exchange2).get("x-delayed-type")).isEqualTo(ExchangeTypes.DIRECT); + assertThat(exchange2.get("type")).isEqualTo("x-delayed-message"); this.rabbitAdmin.deleteQueue(queue.getName()); this.rabbitAdmin.deleteExchange(exchangeName); } - private ExchangeInfo getExchange(String exchangeName) throws Exception { - Client rabbitRestClient = new Client("http://localhost:15672/api/", "guest", "guest"); + private Map getExchange(String exchangeName) throws Exception { return await().pollDelay(Duration.ZERO) - .until(() -> rabbitRestClient.getExchange("/", exchangeName), exch -> exch != null); + .until(() -> exchangeInfo(exchangeName), exch -> exch != null); } /** diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java index d617d52773..9c0f2b8ff3 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java @@ -46,7 +46,6 @@ import java.util.concurrent.TimeoutException; import org.apache.commons.logging.Log; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -85,8 +84,6 @@ import com.rabbitmq.client.AMQP.Queue.DeclareOk; import com.rabbitmq.client.Channel; import com.rabbitmq.client.DefaultConsumer; -import com.rabbitmq.http.client.Client; -import com.rabbitmq.http.client.domain.QueueInfo; /** * @author Mark Pollack @@ -100,8 +97,7 @@ * */ @RabbitAvailable(management = true) -@Disabled("Temporary until SF uses Micrometer snaps") -public class RabbitAdminTests { +public class RabbitAdminTests extends NeedsManagementTests { @Test public void testSettingOfNullConnectionFactory() { @@ -375,17 +371,16 @@ public void testLeaderLocator() throws Exception { RabbitAdmin admin = new RabbitAdmin(cf); AnonymousQueue queue = new AnonymousQueue(); admin.declareQueue(queue); - Client client = new Client("http://guest:guest@localhost:15672/api"); AnonymousQueue queue1 = queue; - QueueInfo info = await().until(() -> client.getQueue("/", queue1.getName()), inf -> inf != null); - assertThat(info.getArguments().get(Queue.X_QUEUE_LEADER_LOCATOR)).isEqualTo("client-local"); + Map info = await().until(() -> queueInfo(queue1.getName()), inf -> inf != null); + assertThat(arguments(info).get(Queue.X_QUEUE_LEADER_LOCATOR)).isEqualTo("client-local"); queue = new AnonymousQueue(); queue.setLeaderLocator(null); admin.declareQueue(queue); AnonymousQueue queue2 = queue; - info = await().until(() -> client.getQueue("/", queue2.getName()), inf -> inf != null); - assertThat(info.getArguments().get(Queue.X_QUEUE_LEADER_LOCATOR)).isNull(); + info = await().until(() -> queueInfo(queue2.getName()), inf -> inf != null); + assertThat(arguments(info).get(Queue.X_QUEUE_LEADER_LOCATOR)).isNull(); cf.destroy(); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitRestApiTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitRestApiTests.java deleted file mode 100644 index a4e5178f8e..0000000000 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitRestApiTests.java +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Copyright 2015-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.core; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.awaitility.Awaitility.await; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URISyntaxException; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -import org.springframework.amqp.AmqpException; -import org.springframework.amqp.core.Binding; -import org.springframework.amqp.core.BindingBuilder; -import org.springframework.amqp.core.DirectExchange; -import org.springframework.amqp.core.Exchange; -import org.springframework.amqp.core.Queue; -import org.springframework.amqp.core.QueueBuilder; -import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; -import org.springframework.amqp.rabbit.junit.RabbitAvailable; - -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.DefaultConsumer; -import com.rabbitmq.http.client.Client; -import com.rabbitmq.http.client.domain.BindingInfo; -import com.rabbitmq.http.client.domain.ExchangeInfo; -import com.rabbitmq.http.client.domain.QueueInfo; - -/** - * @author Gary Russell - * @author Artem Bilan - * - * @since 1.5 - * - */ -@RabbitAvailable(management = true) -@Disabled("Temporary until SF uses Micrometer snaps") -public class RabbitRestApiTests { - - private final CachingConnectionFactory connectionFactory = new CachingConnectionFactory("localhost"); - - private final Client rabbitRestClient; - - public RabbitRestApiTests() throws MalformedURLException, URISyntaxException { - this.rabbitRestClient = new Client("http://localhost:15672/api/", "guest", "guest"); - } - - @AfterEach - public void tearDown() { - connectionFactory.destroy(); - } - - @Test - public void testExchanges() { - List list = this.rabbitRestClient.getExchanges(); - assertThat(list.size() > 0).isTrue(); - } - - @Test - public void testExchangesVhost() { - List list = this.rabbitRestClient.getExchanges("/"); - assertThat(list.size() > 0).isTrue(); - } - - @Test - public void testBindings() { - List list = this.rabbitRestClient.getBindings(); - assertThat(list.size() > 0).isTrue(); - } - - @Test - public void testBindingsVhost() { - List list = this.rabbitRestClient.getBindings("/"); - assertThat(list.size() > 0).isTrue(); - } - - @Test - public void testQueues() { - List list = this.rabbitRestClient.getQueues(); - assertThat(list.size() > 0).isTrue(); - } - - @Test - public void testQueuesVhost() { - List list = this.rabbitRestClient.getQueues("/"); - assertThat(list.size() > 0).isTrue(); - } - - @Test - public void testBindingsDetail() { - RabbitAdmin admin = new RabbitAdmin(connectionFactory); - Map args = Collections.singletonMap("alternate-exchange", ""); - Exchange exchange1 = new DirectExchange(UUID.randomUUID().toString(), false, true, args); - admin.declareExchange(exchange1); - Exchange exchange2 = new DirectExchange(UUID.randomUUID().toString(), false, true, args); - admin.declareExchange(exchange2); - Queue queue = admin.declareQueue(); - Binding binding1 = BindingBuilder - .bind(queue) - .to(exchange1) - .with("foo") - .and(args); - admin.declareBinding(binding1); - Binding binding2 = BindingBuilder - .bind(exchange2) - .to((DirectExchange) exchange1) - .with("bar"); - admin.declareBinding(binding2); - - List bindings = this.rabbitRestClient.getBindingsBySource("/", exchange1.getName()); - assertThat(bindings).hasSize(2); - assertThat(bindings.get(0).getSource()).isEqualTo(exchange1.getName()); - assertThat("foo").isIn(bindings.get(0).getRoutingKey(), bindings.get(1).getRoutingKey()); - BindingInfo qout = null; - BindingInfo eout = null; - if (bindings.get(0).getRoutingKey().equals("foo")) { - qout = bindings.get(0); - eout = bindings.get(1); - } - else { - eout = bindings.get(0); - qout = bindings.get(1); - } - assertThat(qout.getDestinationType()).isEqualTo("queue"); - assertThat(qout.getDestination()).isEqualTo(queue.getName()); - assertThat(qout.getArguments()).isNotNull(); - assertThat(qout.getArguments().get("alternate-exchange")).isEqualTo(""); - - assertThat(eout.getDestinationType()).isEqualTo("exchange"); - assertThat(eout.getDestination()).isEqualTo(exchange2.getName()); - - admin.deleteExchange(exchange1.getName()); - admin.deleteExchange(exchange2.getName()); - } - - @Test - public void testSpecificExchange() { - RabbitAdmin admin = new RabbitAdmin(connectionFactory); - Map args = Collections.singletonMap("alternate-exchange", ""); - Exchange exchange = new DirectExchange(UUID.randomUUID().toString(), true, true, args); - admin.declareExchange(exchange); - ExchangeInfo exchangeOut = this.rabbitRestClient.getExchange("/", exchange.getName()); - assertThat(exchangeOut.isDurable()).isTrue(); - assertThat(exchangeOut.isAutoDelete()).isTrue(); - assertThat(exchangeOut.getName()).isEqualTo(exchange.getName()); - assertThat(exchangeOut.getArguments()).isEqualTo(args); - admin.deleteExchange(exchange.getName()); - } - - @Test - public void testSpecificQueue() throws Exception { - RabbitAdmin admin = new RabbitAdmin(connectionFactory); - Map args = Collections.singletonMap("foo", "bar"); - Queue queue1 = QueueBuilder.nonDurable(UUID.randomUUID().toString()) - .autoDelete() - .withArguments(args) - .build(); - admin.declareQueue(queue1); - Queue queue2 = QueueBuilder.durable(UUID.randomUUID().toString()) - .withArguments(args) - .build(); - admin.declareQueue(queue2); - Channel channel = this.connectionFactory.createConnection().createChannel(false); - String consumer = channel.basicConsume(queue1.getName(), false, "", false, true, null, new DefaultConsumer(channel)); - QueueInfo qi = await().until(() -> this.rabbitRestClient.getQueue("/", queue1.getName()), - info -> info.getExclusiveConsumerTag() != null && !"".equals(info.getExclusiveConsumerTag())); - QueueInfo queueOut = this.rabbitRestClient.getQueue("/", queue1.getName()); - assertThat(queueOut.isDurable()).isFalse(); - assertThat(queueOut.isExclusive()).isFalse(); - assertThat(queueOut.isAutoDelete()).isTrue(); - assertThat(queueOut.getName()).isEqualTo(queue1.getName()); - assertThat(queueOut.getArguments()).isEqualTo(args); - assertThat(qi.getExclusiveConsumerTag()).isEqualTo(consumer); - channel.basicCancel(consumer); - channel.close(); - - queueOut = this.rabbitRestClient.getQueue("/", queue2.getName()); - assertThat(queueOut.isDurable()).isTrue(); - assertThat(queueOut.isExclusive()).isFalse(); - assertThat(queueOut.isAutoDelete()).isFalse(); - assertThat(queueOut.getName()).isEqualTo(queue2.getName()); - assertThat(queueOut.getArguments()).isEqualTo(args); - - admin.deleteQueue(queue1.getName()); - admin.deleteQueue(queue2.getName()); - } - - @Test - public void testDeleteExchange() { - String exchangeName = "testExchange"; - Exchange testExchange = new DirectExchange(exchangeName); - ExchangeInfo info = new ExchangeInfo(); - info.setArguments(testExchange.getArguments()); - info.setAutoDelete(testExchange.isAutoDelete()); - info.setDurable(testExchange.isDurable()); - info.setType(testExchange.getType()); - this.rabbitRestClient.declareExchange("/", testExchange.getName(), info); - ExchangeInfo exchangeToAssert = this.rabbitRestClient.getExchange("/", exchangeName); - assertThat(exchangeToAssert.getName()).isEqualTo(testExchange.getName()); - assertThat(exchangeToAssert.getType()).isEqualTo(testExchange.getType()); - this.rabbitRestClient.deleteExchange("/", testExchange.getName()); - // 6.0.0 REST compatibility -// assertThat(this.rabbitRestClient.getExchange("/", exchangeName)).isNull(); - RabbitTemplate template = new RabbitTemplate(this.connectionFactory); - assertThatExceptionOfType(AmqpException.class) - .isThrownBy(() -> template.execute(channel -> channel.exchangeDeclarePassive(exchangeName))) - .withCauseExactlyInstanceOf(IOException.class); - } - -} diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index 910f41b8d9..e206ebf140 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -795,6 +795,48 @@ public LocalizedQueueConnectionFactory queueAffinityCF( Notice that the first three parameters are arrays of `addresses`, `adminUris`, and `nodes`. These are positional in that, when a container attempts to connect to a queue, it uses the admin API to determine which node is the lead for the queue and connects to the address in the same array position as that node. +IMPORTANT: Starting with version 3.0, the RabbitMQ `http-client` is no longer used to access the Rest API. +Instead, by default, the `WebClient` from Spring Webflux is used; which is not added to the class path by default. + +.Maven +==== +[source,xml,subs="+attributes"] +---- + + org.springframework.amqp + spring-rabbit + +---- +==== +.Gradle +==== +[source,groovy,subs="+attributes"] +---- +compile 'org.springframework.amqp:spring-rabbit' +---- +==== + +You can also use other REST technology by implementing `LocalizedQueueConnectionFactory.NodeLocator` and overriding its `createClient, ``restCall`, and optionally, `close` methods. + +==== +[source, java] +---- +lqcf.setNodeLocator(new DefaultNodeLocator() { + + @Override + public MyClient createClient(String userName, String password) { + ... + } + + @Override + public HashMap restCall(MyClient client, URI uri) { + ... + }); + +}); +---- +==== + [[cf-pub-conf-ret]] ===== Publisher Confirms and Returns diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index d11a5e719d..ebcbe08a35 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -43,3 +43,6 @@ See <> for more information. The default `addressShuffleMode` in `AbstractConnectionFactory` is now `RANDOM`. This results in connecting to a random host when multiple addresses are provided. See <> for more information. + +The `LocalizedQueueConnectionFactory` no longer uses the RabbitMQ `http-client` library to determine which node is the leader for a queue. +See <> for more information. From b1ae21cad96cafd476cf187f73dfe6b20b5e1f8a Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 11 Oct 2022 14:39:39 -0400 Subject: [PATCH 166/737] GH-1419: Add RestTemplateNodeLocator - also remove hard dependency on `spring-webflux` from `spring-rabbit-junit`. * Use reactor-netty-http. * Fix Javadoc. --- build.gradle | 8 +- .../rabbit/junit/BrokerRunningSupport.java | 46 ++++++--- .../LocalizedQueueConnectionFactory.java | 23 ++++- .../rabbit/connection/RestTemplateHolder.java | 41 ++++++++ .../connection/RestTemplateNodeLocator.java | 97 +++++++++++++++++++ .../rabbit/connection/WebFluxNodeLocator.java | 5 +- ...ueueConnectionFactoryIntegrationTests.java | 3 + src/reference/asciidoc/amqp.adoc | 8 +- 8 files changed, 206 insertions(+), 25 deletions(-) create mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateHolder.java create mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateNodeLocator.java diff --git a/build.gradle b/build.gradle index 4ec14970d0..2684b30b7c 100644 --- a/build.gradle +++ b/build.gradle @@ -44,6 +44,7 @@ ext { assertkVersion = '0.24' awaitilityVersion = '4.2.0' commonsCompressVersion = '1.20' + commonsHttpClientVersion = '5.1.3' commonsPoolVersion = '2.11.1' googleJsr305Version = '3.0.2' hamcrestVersion = '2.2' @@ -387,7 +388,9 @@ project('spring-rabbit') { api 'org.springframework:spring-messaging' api 'org.springframework:spring-tx' optionalApi 'org.springframework:spring-webflux' + optionalApi "org.apache.httpcomponents.client5:httpclient5:$commonsHttpClientVersion" optionalApi 'io.projectreactor:reactor-core' + optionalApi 'io.projectreactor.netty:reactor-netty-http' optionalApi "ch.qos.logback:logback-classic:$logbackVersion" optionalApi 'org.apache.logging.log4j:log4j-core' optionalApi 'io.micrometer:micrometer-core' @@ -473,6 +476,7 @@ project('spring-rabbit-stream') { testRuntimeOnly "com.github.luben:zstd-jni:$zstdJniVersion" testImplementation "org.testcontainers:rabbitmq:1.17.3" testImplementation "org.apache.logging.log4j:log4j-slf4j-impl:$log4jVersion" + testImplementation 'org.springframework:spring-webflux' } } @@ -488,14 +492,12 @@ project('spring-rabbit-junit') { exclude group: 'org.hamcrest', module: 'hamcrest-core' } api "com.rabbitmq:amqp-client:$rabbitmqVersion" - api 'org.springframework:spring-webflux' + api 'org.springframework:spring-web' api 'org.junit.jupiter:junit-jupiter-api' api "org.assertj:assertj-core:$assertjVersion" optionalApi "ch.qos.logback:logback-classic:$logbackVersion" optionalApi 'org.apache.logging.log4j:log4j-core' compileOnly 'org.apiguardian:apiguardian-api:1.0.0' - testRuntimeOnly 'com.fasterxml.jackson.core:jackson-core' - testRuntimeOnly 'com.fasterxml.jackson.core:jackson-databind' } } diff --git a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java index 90b2d9f7e3..9caf727a57 100644 --- a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java +++ b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java @@ -17,11 +17,16 @@ package org.springframework.amqp.rabbit.junit; import java.io.IOException; +import java.net.Authenticator; +import java.net.PasswordAuthentication; import java.net.URI; import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -33,12 +38,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.MediaType; import org.springframework.util.Base64Utils; import org.springframework.util.StringUtils; -import org.springframework.web.reactive.function.client.ExchangeFilterFunctions; -import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.util.UriUtils; import com.rabbitmq.client.Channel; @@ -388,22 +389,35 @@ private Channel createQueues(Connection connection) throws IOException, URISynta } private boolean alivenessTest() throws URISyntaxException { - WebClient client = WebClient.builder() - .filter(ExchangeFilterFunctions.basicAuthentication(this.adminUser, this.adminPassword)) + HttpClient client = HttpClient.newBuilder() + .authenticator(new Authenticator() { + + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(getAdminUser(), getAdminPassword().toCharArray()); + } + + }) .build(); URI uri = new URI(getAdminUri()) .resolve("/api/aliveness-test/" + UriUtils.encodePathSegment("/", StandardCharsets.UTF_8)); - HashMap result = client.get() + HttpRequest request = HttpRequest.newBuilder() + .GET() .uri(uri) - .accept(MediaType.APPLICATION_JSON) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() { - }) - .block(Duration.ofSeconds(10)); // NOSONAR magic# - if (result != null) { - return result.get("status").equals("ok"); + .build(); + HttpResponse response; + try { + response = client.send(request, BodyHandlers.ofString()); + } + catch (IOException ex) { + LOGGER.error("Exception checking admin aliveness", ex); + return false; + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + return false; } - return false; + return response.body().contentEquals("{\"status\":\"ok\"}"); } public static boolean fatal() { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java index decb22fa1c..83885e82cc 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java @@ -35,6 +35,7 @@ import org.springframework.core.log.LogAccessor; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; /** * A {@link RoutingConnectionFactory} that determines the node on which a queue is located and @@ -54,6 +55,13 @@ */ public class LocalizedQueueConnectionFactory implements ConnectionFactory, RoutingConnectionFactory, DisposableBean { + private static final boolean USING_WEBFLUX; + + static { + USING_WEBFLUX = ClassUtils.isPresent("org.springframework.web.reactive.function.client.WebClient", + LocalizedQueueConnectionFactory.class.getClassLoader()); + } + private final Log logger = LogFactory.getLog(getClass()); private final Map nodeFactories = new HashMap(); @@ -82,7 +90,7 @@ public class LocalizedQueueConnectionFactory implements ConnectionFactory, Routi private final String trustStorePassPhrase; - private NodeLocator nodeLocator = new WebFluxNodeLocator(); + private NodeLocator nodeLocator; /** * @param defaultConnectionFactory the fallback connection factory to use if the queue @@ -190,6 +198,12 @@ private LocalizedQueueConnectionFactory(ConnectionFactory defaultConnectionFacto this.trustStore = trustStore; this.keyStorePassPhrase = keyStorePassPhrase; this.trustStorePassPhrase = trustStorePassPhrase; + if (USING_WEBFLUX) { + this.nodeLocator = new WebFluxNodeLocator(); + } + else { + this.nodeLocator = new RestTemplateNodeLocator(); + } } private static Map nodesAddressesToMap(String[] nodes, String[] addresses) { @@ -206,6 +220,7 @@ private static Map nodesAddressesToMap(String[] nodes, String[] * @since 2.4.8 */ public void setNodeLocator(NodeLocator nodeLocator) { + Assert.notNull(nodeLocator, "'nodeLocator' cannot be null"); this.nodeLocator = nodeLocator; } @@ -378,7 +393,7 @@ default ConnectionFactory locate(String[] adminUris, Map nodeToA try { String uri = new URI(adminUri) .resolve("/api/queues/").toString(); - HashMap queueInfo = restCall(client, uri, vhost, queue); + Map queueInfo = restCall(client, uri, vhost, queue); if (queueInfo != null) { String node = (String) queueInfo.get("node"); if (node != null) { @@ -423,13 +438,15 @@ default void close(T client) { /** * Retrieve a map of queue properties using the RabbitMQ Management REST API. + * @param client the client. * @param baseUri the base uri. * @param vhost the virtual host. * @param queue the queue name. * @return the map of queue properties. * @throws URISyntaxException if the syntax is bad. */ - HashMap restCall(T client, String baseUri, String vhost, String queue) + @Nullable + Map restCall(T client, String baseUri, String vhost, String queue) throws URISyntaxException; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateHolder.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateHolder.java new file mode 100644 index 0000000000..056435925c --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateHolder.java @@ -0,0 +1,41 @@ +/* + * 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.connection; + +import org.springframework.web.client.RestTemplate; + +/** + * Holder for a {@link RestTemplate} and credentials. + * + * @author Gary Russell + * @since 2.4.8 + * + */ +class RestTemplateHolder { + + final String userName; // NOSONAR + + final String password; // NOSONAR + + RestTemplate template; // NOSONAR + + RestTemplateHolder(String userName, String password) { + this.userName = userName; + this.password = password; + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateNodeLocator.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateNodeLocator.java new file mode 100644 index 0000000000..be7fcae452 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateNodeLocator.java @@ -0,0 +1,97 @@ +/* + * 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.connection; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import org.apache.hc.client5.http.auth.AuthCache; +import org.apache.hc.client5.http.impl.auth.BasicAuthCache; +import org.apache.hc.client5.http.impl.auth.BasicScheme; +import org.apache.hc.client5.http.protocol.HttpClientContext; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.protocol.BasicHttpContext; +import org.apache.hc.core5.http.protocol.HttpContext; + +import org.springframework.amqp.rabbit.connection.LocalizedQueueConnectionFactory.NodeLocator; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.http.client.support.BasicAuthenticationInterceptor; +import org.springframework.lang.Nullable; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriUtils; + +/** + * A {@link NodeLocator} using the {@link RestTemplate}. + * + * @author Gary Russell + * @since 3.0 + * + */ +public class RestTemplateNodeLocator implements NodeLocator { + + @Override + public RestTemplateHolder createClient(String userName, String password) { + return new RestTemplateHolder(userName, password); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + @Nullable + public Map restCall(RestTemplateHolder client, String baseUri, String vhost, String queue) + throws URISyntaxException { + + if (client.template == null) { + URI uri = new URI(baseUri); + HttpHost host = new HttpHost(uri.getHost(), uri.getPort()); + client.template = new RestTemplate(new HttpComponentsClientHttpRequestFactory() { + + @Override + @Nullable + protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri) { + AuthCache cache = new BasicAuthCache(); + BasicScheme scheme = new BasicScheme(); + cache.put(host, scheme); + BasicHttpContext context = new BasicHttpContext(); + context.setAttribute(HttpClientContext.AUTH_CACHE, cache); + return context; + } + + }); + client.template.getInterceptors().add(new BasicAuthenticationInterceptor(client.userName, client.password)); + } + URI uri = new URI(baseUri) + .resolve("/api/queues/" + UriUtils.encodePathSegment(vhost, StandardCharsets.UTF_8) + "/" + queue); + ResponseEntity response = client.template.exchange(uri, HttpMethod.GET, null, Map.class); + return response.getStatusCode().equals(HttpStatus.OK) ? response.getBody() : null; + } + + @Override + public void close(RestTemplateHolder client) { + try { + client.template.close(); + } + catch (IOException e) { + } + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/WebFluxNodeLocator.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/WebFluxNodeLocator.java index bedf67f017..5179ea378a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/WebFluxNodeLocator.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/WebFluxNodeLocator.java @@ -21,10 +21,12 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.HashMap; +import java.util.Map; import org.springframework.amqp.rabbit.connection.LocalizedQueueConnectionFactory.NodeLocator; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; import org.springframework.web.reactive.function.client.ExchangeFilterFunctions; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.util.UriUtils; @@ -39,7 +41,8 @@ public class WebFluxNodeLocator implements NodeLocator { @Override - public HashMap restCall(WebClient client, String baseUri, String vhost, String queue) + @Nullable + public Map restCall(WebClient client, String baseUri, String vhost, String queue) throws URISyntaxException { URI uri = new URI(baseUri) diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryIntegrationTests.java index 9a20edf235..9c2a2dd7d2 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryIntegrationTests.java @@ -83,6 +83,9 @@ void findLocal() { ConnectionFactory cf = lqcf.getTargetConnectionFactory("[local]"); RabbitAdmin admin = new RabbitAdmin(cf); assertThat(admin.getQueueProperties("local")).isNotNull(); + lqcf.setNodeLocator(new RestTemplateNodeLocator()); + ConnectionFactory cf2 = lqcf.getTargetConnectionFactory("[local]"); + assertThat(cf2).isSameAs(cf); lqcf.destroy(); } diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index e206ebf140..36ea2bcacc 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -796,7 +796,9 @@ Notice that the first three parameters are arrays of `addresses`, `adminUris`, a These are positional in that, when a container attempts to connect to a queue, it uses the admin API to determine which node is the lead for the queue and connects to the address in the same array position as that node. IMPORTANT: Starting with version 3.0, the RabbitMQ `http-client` is no longer used to access the Rest API. -Instead, by default, the `WebClient` from Spring Webflux is used; which is not added to the class path by default. +Instead, by default, the `WebClient` from Spring Webflux is used if `spring-webflux` is on the class path; otherwise a `RestTemplate` is used. + +To add `WebFlux` to the class path: .Maven ==== @@ -821,7 +823,7 @@ You can also use other REST technology by implementing `LocalizedQueueConnection ==== [source, java] ---- -lqcf.setNodeLocator(new DefaultNodeLocator() { +lqcf.setNodeLocator(new NodeLocator() { @Override public MyClient createClient(String userName, String password) { @@ -837,6 +839,8 @@ lqcf.setNodeLocator(new DefaultNodeLocator() { ---- ==== +The framework provides the `WebFluxNodeLocator` and `RestTemplateNodeLocator`, with the default as discussed above. + [[cf-pub-conf-ret]] ===== Publisher Confirms and Returns From 8b4dd8665b67fb2386b5f786d03d8a5cccfebf54 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 11 Oct 2022 16:59:22 -0400 Subject: [PATCH 167/737] GH-1514: Pattern-Matched instanceof Where Possible Resolves https://github.com/spring-projects/spring-amqp/issues/1514 --- .../amqp/rabbit/AsyncRabbitTemplate.java | 4 +- ...itListenerAnnotationBeanPostProcessor.java | 89 +++++++++---------- ...RetryOperationsInterceptorFactoryBean.java | 14 +-- .../connection/AbstractConnectionFactory.java | 11 ++- .../connection/CachingConnectionFactory.java | 4 +- .../connection/ClosingRecoveryListener.java | 13 ++- .../LocalizedQueueConnectionFactory.java | 4 +- .../PublisherCallbackChannelImpl.java | 4 +- .../amqp/rabbit/connection/RabbitUtils.java | 54 +++++------ .../rabbit/connection/SimpleConnection.java | 8 +- .../ThreadChannelConnectionFactory.java | 8 +- .../amqp/rabbit/core/RabbitAdmin.java | 40 ++++----- .../rabbit/core/RabbitMessagingTemplate.java | 12 +-- .../amqp/rabbit/core/RabbitTemplate.java | 34 ++++--- .../AbstractMessageListenerContainer.java | 34 ++++--- .../AbstractRabbitListenerEndpoint.java | 11 ++- .../listener/BlockingQueueConsumer.java | 4 +- .../ConditionalRejectingErrorHandler.java | 10 +-- .../DirectMessageListenerContainer.java | 10 +-- .../RabbitListenerEndpointRegistrar.java | 6 +- .../RabbitListenerEndpointRegistry.java | 10 +-- .../SimpleMessageListenerContainer.java | 4 +- .../AbstractAdaptableMessageListener.java | 14 +-- .../adapter/MessageListenerAdapter.java | 14 +-- .../MessagingMessageListenerAdapter.java | 22 +++-- .../listener/support/ContainerUtils.java | 6 +- .../amqp/rabbit/logback/AmqpAppender.java | 8 +- .../DefaultMessagePropertiesConverter.java | 17 ++-- .../support/RabbitExceptionTranslator.java | 14 +-- 29 files changed, 234 insertions(+), 249 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java index 56dcfe9b4a..f670255238 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java @@ -581,8 +581,8 @@ public void onMessage(Message message, Channel channel) { MessageConverter messageConverter = this.template.getMessageConverter(); RabbitConverterFuture rabbitFuture = (RabbitConverterFuture) future; Object converted = rabbitFuture.getReturnType() != null - && messageConverter instanceof SmartMessageConverter - ? ((SmartMessageConverter) messageConverter).fromMessage(message, + && messageConverter instanceof SmartMessageConverter smart + ? smart.fromMessage(message, rabbitFuture.getReturnType()) : messageConverter.fromMessage(message); rabbitFuture.complete(converted); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java index 0ba855e6e3..0f41248539 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java @@ -229,9 +229,9 @@ public void setMessageHandlerMethodFactory(MessageHandlerMethodFactory messageHa @Override public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; - if (beanFactory instanceof ConfigurableListableBeanFactory) { - this.resolver = ((ConfigurableListableBeanFactory) beanFactory).getBeanExpressionResolver(); - this.expressionContext = new BeanExpressionContext((ConfigurableListableBeanFactory) beanFactory, null); + if (beanFactory instanceof ConfigurableListableBeanFactory clbf) { + this.resolver = clbf.getBeanExpressionResolver(); + this.expressionContext = new BeanExpressionContext(clbf, null); } } @@ -265,9 +265,9 @@ MessageHandlerMethodFactory getMessageHandlerMethodFactory() { public void afterSingletonsInstantiated() { this.registrar.setBeanFactory(this.beanFactory); - if (this.beanFactory instanceof ListableBeanFactory) { + if (this.beanFactory instanceof ListableBeanFactory lbf) { Map instances = - ((ListableBeanFactory) this.beanFactory).getBeansOfType(RabbitListenerConfigurer.class); + lbf.getBeansOfType(RabbitListenerConfigurer.class); for (RabbitListenerConfigurer configurer : instances.values()) { configurer.configureRabbitListeners(this.registrar); } @@ -445,8 +445,8 @@ protected Collection processListener(MethodRabbitListenerEndpoint en String group = rabbitListener.group(); if (StringUtils.hasText(group)) { Object resolvedGroup = resolveExpression(group); - if (resolvedGroup instanceof String) { - endpoint.setGroup((String) resolvedGroup); + if (resolvedGroup instanceof String str) { + endpoint.setGroup(str); } } String autoStartup = rabbitListener.autoStartup(); @@ -483,8 +483,8 @@ protected Collection processListener(MethodRabbitListenerEndpoint en private void resolveErrorHandler(MethodRabbitListenerEndpoint endpoint, RabbitListener rabbitListener) { Object errorHandler = resolveExpression(rabbitListener.errorHandler()); - if (errorHandler instanceof RabbitListenerErrorHandler) { - endpoint.setErrorHandler((RabbitListenerErrorHandler) errorHandler); + if (errorHandler instanceof RabbitListenerErrorHandler rleh) { + endpoint.setErrorHandler(rleh); } else { String errorHandlerBeanName = resolveExpressionAsString(rabbitListener.errorHandler(), "errorHandler"); @@ -499,11 +499,11 @@ private void resolveAckMode(MethodRabbitListenerEndpoint endpoint, RabbitListene String ackModeAttr = rabbitListener.ackMode(); if (StringUtils.hasText(ackModeAttr)) { Object ackMode = resolveExpression(ackModeAttr); - if (ackMode instanceof String) { - endpoint.setAckMode(AcknowledgeMode.valueOf((String) ackMode)); + if (ackMode instanceof String str) { + endpoint.setAckMode(AcknowledgeMode.valueOf(str)); } - else if (ackMode instanceof AcknowledgeMode) { - endpoint.setAckMode((AcknowledgeMode) ackMode); + else if (ackMode instanceof AcknowledgeMode mode) { + endpoint.setAckMode(mode); } else { Assert.isNull(ackMode, "ackMode must resolve to a String or AcknowledgeMode"); @@ -513,8 +513,8 @@ else if (ackMode instanceof AcknowledgeMode) { private void resolveAdmin(MethodRabbitListenerEndpoint endpoint, RabbitListener rabbitListener, Object adminTarget) { Object resolved = resolveExpression(rabbitListener.admin()); - if (resolved instanceof AmqpAdmin) { - endpoint.setAdmin((AmqpAdmin) resolved); + if (resolved instanceof AmqpAdmin admin) { + endpoint.setAdmin(admin); } else { String rabbitAdmin = resolveExpressionAsString(rabbitListener.admin(), "admin"); @@ -538,8 +538,8 @@ private RabbitListenerContainerFactory resolveContainerFactory(RabbitListener RabbitListenerContainerFactory factory = null; Object resolved = resolveExpression(rabbitListener.containerFactory()); - if (resolved instanceof RabbitListenerContainerFactory) { - return (RabbitListenerContainerFactory) resolved; + if (resolved instanceof RabbitListenerContainerFactory rlcf) { + return rlcf; } String containerFactoryBeanName = resolveExpressionAsString(rabbitListener.containerFactory(), "containerFactory"); @@ -561,8 +561,8 @@ private void resolveExecutor(MethodRabbitListenerEndpoint endpoint, RabbitListen Object execTarget, String beanName) { Object resolved = resolveExpression(rabbitListener.executor()); - if (resolved instanceof TaskExecutor) { - endpoint.setTaskExecutor((TaskExecutor) resolved); + if (resolved instanceof TaskExecutor tex) { + endpoint.setTaskExecutor(tex); } else { String execBeanName = resolveExpressionAsString(rabbitListener.executor(), "executor"); @@ -583,8 +583,8 @@ private void resolvePostProcessor(MethodRabbitListenerEndpoint endpoint, RabbitL Object target, String beanName) { Object resolved = resolveExpression(rabbitListener.replyPostProcessor()); - if (resolved instanceof ReplyPostProcessor) { - endpoint.setReplyPostProcessor((ReplyPostProcessor) resolved); + if (resolved instanceof ReplyPostProcessor rpp) { + endpoint.setReplyPostProcessor(rpp); } else { String ppBeanName = resolveExpressionAsString(rabbitListener.replyPostProcessor(), "replyPostProcessor"); @@ -605,8 +605,8 @@ private void resolveMessageConverter(MethodRabbitListenerEndpoint endpoint, Rabb Object target, String beanName) { Object resolved = resolveExpression(rabbitListener.messageConverter()); - if (resolved instanceof MessageConverter) { - endpoint.setMessageConverter((MessageConverter) resolved); + if (resolved instanceof MessageConverter converter) { + endpoint.setMessageConverter(converter); } else { String mcBeanName = resolveExpressionAsString(rabbitListener.messageConverter(), "messageConverter"); @@ -704,20 +704,20 @@ private void resolveAsStringOrQueue(Object resolvedValue, List names, @N String what) { Object resolvedValueToUse = resolvedValue; - if (resolvedValue instanceof String[]) { - resolvedValueToUse = Arrays.asList((String[]) resolvedValue); + if (resolvedValue instanceof String[] strings) { + resolvedValueToUse = Arrays.asList(strings); } - if (queues != null && resolvedValueToUse instanceof Queue) { + if (queues != null && resolvedValueToUse instanceof Queue q) { if (!names.isEmpty()) { // revert to the previous behavior of just using the name when there is mixture of String and Queue - names.add(((Queue) resolvedValueToUse).getName()); + names.add(q.getName()); } else { - queues.add((Queue) resolvedValueToUse); + queues.add(q); } } - else if (resolvedValueToUse instanceof String) { - names.add((String) resolvedValueToUse); + else if (resolvedValueToUse instanceof String str) { + names.add(str); } else if (resolvedValueToUse instanceof Iterable) { for (Object object : (Iterable) resolvedValueToUse) { @@ -858,8 +858,8 @@ private Map resolveArguments(Argument[] arguments) { Object type = resolveExpression(arg.type()); Class typeClass; String typeName; - if (type instanceof Class) { - typeClass = (Class) type; + if (type instanceof Class clazz) { + typeClass = clazz; typeName = typeClass.getName(); } else { @@ -924,12 +924,11 @@ private boolean resolveExpressionAsBoolean(String value) { private boolean resolveExpressionAsBoolean(String value, boolean defaultValue) { Object resolved = resolveExpression(value); - if (resolved instanceof Boolean) { - return (Boolean) resolved; + if (resolved instanceof Boolean bool) { + return bool; } - else if (resolved instanceof String) { - final String s = (String) resolved; - return StringUtils.hasText(s) ? Boolean.parseBoolean(s) : defaultValue; + else if (resolved instanceof String str) { + return StringUtils.hasText(str) ? Boolean.parseBoolean(str) : defaultValue; } else { return defaultValue; @@ -938,8 +937,8 @@ else if (resolved instanceof String) { protected String resolveExpressionAsString(String value, String attribute) { Object resolved = resolveExpression(value); - if (resolved instanceof String) { - return (String) resolved; + if (resolved instanceof String str) { + return str; } else { throw new IllegalStateException("The [" + attribute + "] must resolve to a String. " @@ -952,8 +951,8 @@ private String resolveExpressionAsStringOrInteger(String value, String attribute return null; } Object resolved = resolveExpression(value); - if (resolved instanceof String) { - return (String) resolved; + if (resolved instanceof String str) { + return str; } else if (resolved instanceof Integer) { return resolved.toString(); @@ -977,8 +976,8 @@ private Object resolveExpression(String value) { * @see ConfigurableBeanFactory#resolveEmbeddedValue */ private String resolve(String value) { - if (this.beanFactory != null && this.beanFactory instanceof ConfigurableBeanFactory) { - return ((ConfigurableBeanFactory) this.beanFactory).resolveEmbeddedValue(value); + if (this.beanFactory != null && this.beanFactory instanceof ConfigurableBeanFactory cbf) { + return cbf.resolveEmbeddedValue(value); } return value; } @@ -1086,8 +1085,8 @@ public Object resolveArgument(MethodParameter parameter, Message message) thr } private boolean isOptional(Message message, Type type) { - return (Optional.class.equals(type) || (type instanceof ParameterizedType - && Optional.class.equals(((ParameterizedType) type).getRawType()))) + return (Optional.class.equals(type) || (type instanceof ParameterizedType pType + && Optional.class.equals(pType.getRawType()))) && message.getPayload().equals(Optional.empty()); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatefulRetryOperationsInterceptorFactoryBean.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatefulRetryOperationsInterceptorFactoryBean.java index 2a89750059..3aa40f94ef 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatefulRetryOperationsInterceptorFactoryBean.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatefulRetryOperationsInterceptorFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -104,11 +104,11 @@ private MethodInvocationRecoverer createRecoverer() { if (messageRecoverer == null) { logger.warn("Message(s) dropped on recovery: " + arg, cause); } - else if (arg instanceof Message) { - messageRecoverer.recover((Message) arg, cause); + else if (arg instanceof Message msg) { + messageRecoverer.recover(msg, cause); } - else if (arg instanceof List && messageRecoverer instanceof MessageBatchRecoverer) { - ((MessageBatchRecoverer) messageRecoverer).recover((List) arg, cause); + else if (arg instanceof List && messageRecoverer instanceof MessageBatchRecoverer recoverer) { + recoverer.recover((List) arg, cause); } // This is actually a normal outcome. It means the recovery was successful, but we don't want to consume // any more messages until the acks and commits are sent for this (problematic) message... @@ -137,8 +137,8 @@ private MethodArgumentsKeyGenerator createKeyGenerator() { private Message argToMessage(Object[] args) { Object arg = args[1]; Message message = null; - if (arg instanceof Message) { - message = (Message) arg; + if (arg instanceof Message msg) { + message = msg; } else if (arg instanceof List) { message = ((List) arg).get(0); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java index f17411c9dc..3bc3bf9510 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java @@ -550,8 +550,8 @@ protected final Connection createBareConnection() { com.rabbitmq.client.Connection rabbitConnection = connect(connectionName); Connection connection = new SimpleConnection(rabbitConnection, this.closeTimeout); - if (rabbitConnection instanceof AutorecoveringConnection) { - ((AutorecoveringConnection) rabbitConnection).addRecoveryListener(new RecoveryListener() { + if (rabbitConnection instanceof AutorecoveringConnection auto) { + auto.addRecoveryListener(new RecoveryListener() { @Override public void handleRecoveryStarted(Recoverable recoverable) { @@ -573,8 +573,8 @@ public void handleRecovery(Recoverable recoverable) { if (this.logger.isInfoEnabled()) { this.logger.info("Created new connection: " + connectionName + "/" + connection); } - if (this.recoveryListener != null && rabbitConnection instanceof AutorecoveringConnection) { - ((AutorecoveringConnection) rabbitConnection).addRecoveryListener(this.recoveryListener); + if (this.recoveryListener != null && rabbitConnection instanceof AutorecoveringConnection auto) { + auto.addRecoveryListener(this.recoveryListener); } if (this.applicationEventPublisher != null) { @@ -717,8 +717,7 @@ private static class DefaultChannelCloseLogger implements ConditionalExceptionLo @Override public void log(Log logger, String message, Throwable t) { - if (t instanceof ShutdownSignalException) { - ShutdownSignalException cause = (ShutdownSignalException) t; + if (t instanceof ShutdownSignalException cause) { if (RabbitUtils.isPassiveDeclarationChannelClose(cause)) { if (logger.isDebugEnabled()) { logger.debug(message + ": " + cause.getMessage()); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java index 6ac59839a2..1397d1d472 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java @@ -1291,8 +1291,8 @@ private void physicalClose(Object proxy) throws IOException, TimeoutException { } else { this.target.close(); - if (this.target instanceof AutorecoveringChannel) { - ClosingRecoveryListener.removeChannel((AutorecoveringChannel) this.target); + if (this.target instanceof AutorecoveringChannel auto) { + ClosingRecoveryListener.removeChannel(auto); } } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ClosingRecoveryListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ClosingRecoveryListener.java index 1f66d5605c..e9a828b7fc 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ClosingRecoveryListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ClosingRecoveryListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-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. @@ -79,14 +79,13 @@ public void handleRecoveryStarted(Recoverable recoverable) { */ public static void addRecoveryListenerIfNecessary(Channel channel) { AutorecoveringChannel autorecoveringChannel = null; - if (channel instanceof ChannelProxy) { - if (((ChannelProxy) channel).getTargetChannel() instanceof AutorecoveringChannel) { - autorecoveringChannel = (AutorecoveringChannel) ((ChannelProxy) channel) - .getTargetChannel(); + if (channel instanceof ChannelProxy proxy) { + if (proxy.getTargetChannel() instanceof AutorecoveringChannel auto) { + autorecoveringChannel = auto; } } - else if (channel instanceof AutorecoveringChannel) { - autorecoveringChannel = (AutorecoveringChannel) channel; + else if (channel instanceof AutorecoveringChannel auto) { + autorecoveringChannel = auto; } if (autorecoveringChannel != null && hasListener.putIfAbsent(autorecoveringChannel, Boolean.TRUE) == null) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java index 83885e82cc..aa25c7a5ff 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java @@ -339,9 +339,9 @@ protected ConnectionFactory createConnectionFactory(String address, String node) public void resetConnection() { Exception lastException = null; for (ConnectionFactory connectionFactory : this.nodeFactories.values()) { - if (connectionFactory instanceof DisposableBean) { + if (connectionFactory instanceof DisposableBean disposable) { try { - ((DisposableBean) connectionFactory).destroy(); + disposable.destroy(); } catch (Exception e) { lastException = e; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java index d1c695d9e8..70ba93125c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java @@ -179,8 +179,8 @@ public Connection getConnection() { @Override public void close(int closeCode, String closeMessage) throws IOException, TimeoutException { this.delegate.close(closeCode, closeMessage); - if (this.delegate instanceof AutorecoveringChannel) { - ClosingRecoveryListener.removeChannel((AutorecoveringChannel) this.delegate); + if (this.delegate instanceof AutorecoveringChannel auto) { + ClosingRecoveryListener.removeChannel(auto); } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java index d52e769ca8..1b42b8c80a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-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. @@ -156,8 +156,8 @@ public static void rollbackIfNecessary(Channel channel) { } public static void closeMessageConsumer(Channel channel, Collection consumerTags, boolean transactional) { - if (!channel.isOpen() && !(channel instanceof ChannelProxy - && ((ChannelProxy) channel).getTargetChannel() instanceof AutorecoveringChannel) + if (!channel.isOpen() && !(channel instanceof ChannelProxy proxy + && proxy.getTargetChannel() instanceof AutorecoveringChannel) && !(channel instanceof AutorecoveringChannel)) { return; } @@ -251,9 +251,9 @@ public static void clearPhysicalCloseRequired() { */ public static boolean isNormalShutdown(ShutdownSignalException sig) { Method shutdownReason = sig.getReason(); - return shutdownReason instanceof AMQP.Connection.Close - && AMQP.REPLY_SUCCESS == ((AMQP.Connection.Close) shutdownReason).getReplyCode() - && "OK".equals(((AMQP.Connection.Close) shutdownReason).getReplyText()); + return shutdownReason instanceof AMQP.Connection.Close closeReason + && AMQP.REPLY_SUCCESS == closeReason.getReplyCode() + && "OK".equals(closeReason.getReplyText()); } /** @@ -265,9 +265,9 @@ public static boolean isNormalShutdown(ShutdownSignalException sig) { public static boolean isNormalChannelClose(ShutdownSignalException sig) { Method shutdownReason = sig.getReason(); return isNormalShutdown(sig) || - (shutdownReason instanceof AMQP.Channel.Close - && AMQP.REPLY_SUCCESS == ((AMQP.Channel.Close) shutdownReason).getReplyCode() - && "OK".equals(((AMQP.Channel.Close) shutdownReason).getReplyText())); + (shutdownReason instanceof AMQP.Channel.Close closeReason + && AMQP.REPLY_SUCCESS == closeReason.getReplyCode() + && "OK".equals(closeReason.getReplyText())); } /** @@ -278,11 +278,11 @@ public static boolean isNormalChannelClose(ShutdownSignalException sig) { */ public static boolean isPassiveDeclarationChannelClose(ShutdownSignalException sig) { Method shutdownReason = sig.getReason(); - return shutdownReason instanceof AMQP.Channel.Close // NOSONAR boolean complexity - && AMQP.NOT_FOUND == ((AMQP.Channel.Close) shutdownReason).getReplyCode() - && ((((AMQP.Channel.Close) shutdownReason).getClassId() == EXCHANGE_CLASS_ID_40 - || ((AMQP.Channel.Close) shutdownReason).getClassId() == QUEUE_CLASS_ID_50) - && ((AMQP.Channel.Close) shutdownReason).getMethodId() == DECLARE_METHOD_ID_10); + return shutdownReason instanceof AMQP.Channel.Close closeReason // NOSONAR boolean complexity + && AMQP.NOT_FOUND == closeReason.getReplyCode() + && ((closeReason.getClassId() == EXCHANGE_CLASS_ID_40 + || closeReason.getClassId() == QUEUE_CLASS_ID_50) + && closeReason.getMethodId() == DECLARE_METHOD_ID_10); } /** @@ -294,11 +294,11 @@ public static boolean isPassiveDeclarationChannelClose(ShutdownSignalException s */ public static boolean isExclusiveUseChannelClose(ShutdownSignalException sig) { Method shutdownReason = sig.getReason(); - return shutdownReason instanceof AMQP.Channel.Close // NOSONAR boolean complexity - && AMQP.ACCESS_REFUSED == ((AMQP.Channel.Close) shutdownReason).getReplyCode() - && ((AMQP.Channel.Close) shutdownReason).getClassId() == BASIC_CLASS_ID_60 - && ((AMQP.Channel.Close) shutdownReason).getMethodId() == CONSUME_METHOD_ID_20 - && ((AMQP.Channel.Close) shutdownReason).getReplyText().contains("exclusive"); + return shutdownReason instanceof AMQP.Channel.Close closeReason // NOSONAR boolean complexity + && AMQP.ACCESS_REFUSED == closeReason.getReplyCode() + && closeReason.getClassId() == BASIC_CLASS_ID_60 + && closeReason.getMethodId() == CONSUME_METHOD_ID_20 + && closeReason.getReplyText().contains("exclusive"); } /** @@ -324,10 +324,10 @@ public static boolean isMismatchedQueueArgs(Exception e) { } else { Method shutdownReason = sig.getReason(); - return shutdownReason instanceof AMQP.Channel.Close - && AMQP.PRECONDITION_FAILED == ((AMQP.Channel.Close) shutdownReason).getReplyCode() - && ((AMQP.Channel.Close) shutdownReason).getClassId() == QUEUE_CLASS_ID_50 - && ((AMQP.Channel.Close) shutdownReason).getMethodId() == DECLARE_METHOD_ID_10; + return shutdownReason instanceof AMQP.Channel.Close closeReason + && AMQP.PRECONDITION_FAILED == closeReason.getReplyCode() + && closeReason.getClassId() == QUEUE_CLASS_ID_50 + && closeReason.getMethodId() == DECLARE_METHOD_ID_10; } } @@ -354,10 +354,10 @@ public static boolean isExchangeDeclarationFailure(Exception e) { } else { Method shutdownReason = sig.getReason(); - return shutdownReason instanceof AMQP.Connection.Close - && AMQP.COMMAND_INVALID == ((AMQP.Connection.Close) shutdownReason).getReplyCode() - && ((AMQP.Connection.Close) shutdownReason).getClassId() == EXCHANGE_CLASS_ID_40 - && ((AMQP.Connection.Close) shutdownReason).getMethodId() == DECLARE_METHOD_ID_10; + return shutdownReason instanceof AMQP.Connection.Close closeReason + && AMQP.COMMAND_INVALID == closeReason.getReplyCode() + && closeReason.getClassId() == EXCHANGE_CLASS_ID_40 + && closeReason.getMethodId() == DECLARE_METHOD_ID_10; } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleConnection.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleConnection.java index c59aadc2a1..4c14f1d02f 100755 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleConnection.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleConnection.java @@ -109,8 +109,8 @@ public boolean isOpen() { @Override public int getLocalPort() { - if (this.delegate instanceof NetworkConnection) { - return ((NetworkConnection) this.delegate).getLocalPort(); + if (this.delegate instanceof NetworkConnection networkConn) { + return networkConn.getLocalPort(); } return 0; } @@ -127,8 +127,8 @@ public boolean removeBlockedListener(BlockedListener listener) { @Override public InetAddress getLocalAddress() { - if (this.delegate instanceof NetworkConnection) { - return ((NetworkConnection) this.delegate).getLocalAddress(); + if (this.delegate instanceof NetworkConnection networkConn) { + return networkConn.getLocalAddress(); } return null; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java index 0eb4593401..6433b324d1 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java @@ -179,8 +179,8 @@ public Object prepareSwitchContext() { @Nullable Object prepareSwitchContext(UUID uuid) { Object pubContext = null; - if (getPublisherConnectionFactory() instanceof ThreadChannelConnectionFactory) { - pubContext = ((ThreadChannelConnectionFactory) getPublisherConnectionFactory()).prepareSwitchContext(uuid); // NOSONAR + if (getPublisherConnectionFactory() instanceof ThreadChannelConnectionFactory tccf) { + pubContext = tccf.prepareSwitchContext(uuid); } Context context = ((ConnectionWrapper) createConnection()).prepareSwitchContext(); if (context.getNonTx() == null && context.getTx() == null) { @@ -214,8 +214,8 @@ public void switchContext(@Nullable Object toSwitch) { boolean doSwitch(Object toSwitch) { boolean switched = false; - if (getPublisherConnectionFactory() instanceof ThreadChannelConnectionFactory) { - switched = ((ThreadChannelConnectionFactory) getPublisherConnectionFactory()).doSwitch(toSwitch); // NOSONAR + if (getPublisherConnectionFactory() instanceof ThreadChannelConnectionFactory tccf) { + switched = tccf.doSwitch(toSwitch); // NOSONAR } Context context = this.contextSwitches.remove(toSwitch); this.switchesInProgress.remove(toSwitch); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java index 9a2be6b7eb..6644c447e4 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -264,8 +264,7 @@ private void removeExchangeBindings(final String exchangeName) { Iterator> iterator = this.manualDeclarables.entrySet().iterator(); while (iterator.hasNext()) { Entry next = iterator.next(); - if (next.getValue() instanceof Binding) { - Binding binding = (Binding) next.getValue(); + if (next.getValue() instanceof Binding binding) { if ((!binding.isDestinationQueue() && binding.getDestination().equals(exchangeName)) || binding.getExchange().equals(exchangeName)) { iterator.remove(); @@ -363,8 +362,7 @@ private void removeQueueBindings(final String queueName) { Iterator> iterator = this.manualDeclarables.entrySet().iterator(); while (iterator.hasNext()) { Entry next = iterator.next(); - if (next.getValue() instanceof Binding) { - Binding binding = (Binding) next.getValue(); + if (next.getValue() instanceof Binding binding) { if (binding.isDestinationQueue() && binding.getDestination().equals(queueName)) { iterator.remove(); } @@ -472,8 +470,8 @@ public QueueInformation getQueueInfo(String queueName) { e); } try { - if (channel instanceof ChannelProxy) { - ((ChannelProxy) channel).getTargetChannel().close(); + if (channel instanceof ChannelProxy proxy) { + proxy.getTargetChannel().close(); } } catch (@SuppressWarnings(UNUSED) TimeoutException e1) { @@ -592,8 +590,8 @@ public void afterPropertiesSet() { backOffPolicy.setMaxInterval(DECLARE_MAX_RETRY_INTERVAL); this.retryTemplate.setBackOffPolicy(backOffPolicy); } - if (this.connectionFactory instanceof CachingConnectionFactory && - ((CachingConnectionFactory) this.connectionFactory).getCacheMode() == CacheMode.CONNECTION) { + if (this.connectionFactory instanceof CachingConnectionFactory ccf && + ccf.getCacheMode() == CacheMode.CONNECTION) { this.logger.warn("RabbitAdmin auto declaration is not supported with CacheMode.CONNECTION"); return; } @@ -698,11 +696,11 @@ public void initialize() { synchronized (this.manualDeclarables) { this.logger.debug("Redeclaring manually declared Declarables"); for (Declarable dec : this.manualDeclarables.values()) { - if (dec instanceof Queue) { - declareQueue((Queue) dec); + if (dec instanceof Queue queue) { + declareQueue(queue); } - else if (dec instanceof Exchange) { - declareExchange((Exchange) dec); + else if (dec instanceof Exchange exch) { + declareExchange(exch); } else { declareBinding((Binding) dec); @@ -731,14 +729,14 @@ private void processDeclarables(Collection contextExchanges, Collectio .values(); declarables.forEach(d -> { d.getDeclarables().forEach(declarable -> { - if (declarable instanceof Exchange) { - contextExchanges.add((Exchange) declarable); + if (declarable instanceof Exchange exch) { + contextExchanges.add(exch); } - else if (declarable instanceof Queue) { - contextQueues.add((Queue) declarable); + else if (declarable instanceof Queue queue) { + contextQueues.add(queue); } - else if (declarable instanceof Binding) { - contextBindings.add((Binding) declarable); + else if (declarable instanceof Binding binding) { + contextBindings.add(binding); } }); }); @@ -847,8 +845,8 @@ private void closeChannelAfterIllegalArg(final Channel channel, Queue queue) { this.logger.error("Exception while declaring queue: '" + queue.getName() + "'"); } try { - if (channel instanceof ChannelProxy) { - ((ChannelProxy) channel).getTargetChannel().close(); + if (channel instanceof ChannelProxy proxy) { + proxy.getTargetChannel().close(); } } catch (IOException | TimeoutException e1) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplate.java index 0f64eac1a2..304200fea7 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplate.java @@ -213,8 +213,8 @@ public T convertSendAndReceive(String exchange, String routingKey, Object re protected void doSend(String destination, Message message) { try { Object correlation = message.getHeaders().get(AmqpHeaders.PUBLISH_CONFIRM_CORRELATION); - if (correlation instanceof CorrelationData) { - this.rabbitTemplate.send(destination, createMessage(message), (CorrelationData) correlation); + if (correlation instanceof CorrelationData corrData) { + this.rabbitTemplate.send(destination, createMessage(message), corrData); } else { this.rabbitTemplate.send(destination, createMessage(message)); @@ -228,8 +228,8 @@ protected void doSend(String destination, Message message) { protected void doSend(String exchange, String routingKey, Message message) { try { Object correlation = message.getHeaders().get(AmqpHeaders.PUBLISH_CONFIRM_CORRELATION); - if (correlation instanceof CorrelationData) { - this.rabbitTemplate.send(exchange, routingKey, createMessage(message), (CorrelationData) correlation); + if (correlation instanceof CorrelationData corrData) { + this.rabbitTemplate.send(exchange, routingKey, createMessage(message), corrData); } else { this.rabbitTemplate.send(exchange, routingKey, createMessage(message)); @@ -324,8 +324,8 @@ protected Message convertAmqpMessage(@Nullable org.springframework.amqp.core. @SuppressWarnings("ThrowableResultOfMethodCallIgnored") protected MessagingException convertAmqpException(RuntimeException ex) { - if (ex instanceof MessagingException) { - return (MessagingException) ex; + if (ex instanceof MessagingException mex) { + return mex; } if (ex instanceof org.springframework.amqp.support.converter.MessageConversionException) { return new MessageConversionException(ex.getMessage(), ex); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java index 639d74163a..7b4efd9e80 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java @@ -1098,9 +1098,7 @@ && isMandatoryFor(message), } private ConnectionFactory obtainTargetConnectionFactory(Expression expression, @Nullable Object rootObject) { - if (expression != null && getConnectionFactory() instanceof AbstractRoutingConnectionFactory) { - AbstractRoutingConnectionFactory routingConnectionFactory = - (AbstractRoutingConnectionFactory) getConnectionFactory(); + if (expression != null && getConnectionFactory() instanceof AbstractRoutingConnectionFactory routingCF) { Object lookupKey; if (rootObject != null) { lookupKey = expression.getValue(this.evaluationContext, rootObject); @@ -1109,11 +1107,11 @@ private ConnectionFactory obtainTargetConnectionFactory(Expression expression, @ lookupKey = expression.getValue(this.evaluationContext); } if (lookupKey != null) { - ConnectionFactory connectionFactory = routingConnectionFactory.getTargetConnectionFactory(lookupKey); + ConnectionFactory connectionFactory = routingCF.getTargetConnectionFactory(lookupKey); if (connectionFactory != null) { return connectionFactory; } - else if (!routingConnectionFactory.isLenientFallback()) { + else if (!routingCF.isLenientFallback()) { throw new IllegalStateException("Cannot determine target ConnectionFactory for lookup key [" + lookupKey + "]"); } @@ -1843,8 +1841,8 @@ protected Message convertSendAndReceiveRaw(final String exchange, final String r } protected Message convertMessageIfNecessary(final Object object) { - if (object instanceof Message) { - return (Message) object; + if (object instanceof Message msg) { + return msg; } return getRequiredMessageConverter().toMessage(object, new MessageProperties()); } @@ -2316,8 +2314,8 @@ public T invoke(OperationsCallback action, @Nullable com.rabbitmq.client. private ConfirmListener addConfirmListener(@Nullable com.rabbitmq.client.ConfirmCallback acks, @Nullable com.rabbitmq.client.ConfirmCallback nacks, Channel channel) { ConfirmListener listener = null; - if (acks != null && nacks != null && channel instanceof ChannelProxy - && ((ChannelProxy) channel).isConfirmSelected()) { + if (acks != null && nacks != null && channel instanceof ChannelProxy proxy + && proxy.isConfirmSelected()) { listener = channel.addConfirmListener(acks, nacks); } return listener; @@ -2474,14 +2472,13 @@ protected void sendToRabbit(Channel channel, String exchange, String routingKey, } private void setupConfirm(Channel channel, Message message, @Nullable CorrelationData correlationDataArg) { - final boolean publisherConfirms = channel instanceof ChannelProxy - && ((ChannelProxy) channel).isPublisherConfirms(); + final boolean publisherConfirms = channel instanceof ChannelProxy proxy + && proxy.isPublisherConfirms(); if ((publisherConfirms || this.confirmCallback != null) - && channel instanceof PublisherCallbackChannel) { + && channel instanceof PublisherCallbackChannel publisherCallbackChannel) { long nextPublishSeqNo = channel.getNextPublishSeqNo(); if (nextPublishSeqNo > 0) { - PublisherCallbackChannel publisherCallbackChannel = (PublisherCallbackChannel) channel; CorrelationData correlationData = this.correlationDataPostProcessor != null ? this.correlationDataPostProcessor.postProcess(message, correlationDataArg) : correlationDataArg; @@ -2497,7 +2494,7 @@ private void setupConfirm(Channel channel, Message message, @Nullable Correlatio logger.debug("Factory does not have confirms enabled"); } } - else if (channel instanceof ChannelProxy && ((ChannelProxy) channel).isConfirmSelected()) { + else if (channel instanceof ChannelProxy proxy && proxy.isConfirmSelected()) { long nextPublishSeqNo = channel.getNextPublishSeqNo(); message.getMessageProperties().setPublishSequenceNumber(nextPublishSeqNo); } @@ -2596,9 +2593,8 @@ private Address getReplyToAddress(Message request) throws AmqpException { * @since 2.0 */ public void addListener(Channel channel) { - if (channel instanceof PublisherCallbackChannel) { - PublisherCallbackChannel publisherCallbackChannel = (PublisherCallbackChannel) channel; - Channel key = channel instanceof ChannelProxy ? ((ChannelProxy) channel).getTargetChannel() : channel; + if (channel instanceof PublisherCallbackChannel publisherCallbackChannel) { + Channel key = channel instanceof ChannelProxy proxy ? proxy.getTargetChannel() : channel; if (this.publisherConfirmChannels.putIfAbsent(key, this) == null) { publisherCallbackChannel.addListener(this); if (logger.isDebugEnabled()) { @@ -2760,8 +2756,8 @@ public void handleDelivery(String consumerTag, Envelope envelope, BasicPropertie }; channel.basicConsume(queueName, consumer); if (!latch.await(timeoutMillis, TimeUnit.MILLISECONDS)) { - if (channel instanceof ChannelProxy) { - ((ChannelProxy) channel).getTargetChannel().close(); + if (channel instanceof ChannelProxy proxy) { + proxy.getTargetChannel().close(); } future.completeExceptionally( new ConsumeOkNotReceivedException("Blocking receive, consumer failed to consume within " diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index 1ddd3a738e..28e621136a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -627,8 +627,8 @@ public final void setApplicationContext(ApplicationContext applicationContext) { @Override public ConnectionFactory getConnectionFactory() { ConnectionFactory connectionFactory = super.getConnectionFactory(); - if (connectionFactory instanceof RoutingConnectionFactory) { - ConnectionFactory targetConnectionFactory = ((RoutingConnectionFactory) connectionFactory) + if (connectionFactory instanceof RoutingConnectionFactory rcf) { + ConnectionFactory targetConnectionFactory = rcf .getTargetConnectionFactory(getRoutingLookupKey()); // NOSONAR never null if (targetConnectionFactory != null) { return targetConnectionFactory; @@ -696,9 +696,7 @@ private String queuesAsListString() { */ @Nullable protected RoutingConnectionFactory getRoutingConnectionFactory() { - return super.getConnectionFactory() instanceof RoutingConnectionFactory - ? (RoutingConnectionFactory) super.getConnectionFactory() - : null; + return super.getConnectionFactory() instanceof RoutingConnectionFactory rcf ? rcf : null; } /** @@ -1565,20 +1563,20 @@ protected void executeListenerAndHandleException(Channel channel, Object data) { try { doExecuteListener(channel, data); if (sample != null) { - this.micrometerHolder.success(sample, data instanceof Message - ? ((Message) data).getMessageProperties().getConsumerQueue() + this.micrometerHolder.success(sample, data instanceof Message message + ? message.getMessageProperties().getConsumerQueue() : queuesAsListString()); } } catch (RuntimeException ex) { if (sample != null) { - this.micrometerHolder.failure(sample, data instanceof Message - ? ((Message) data).getMessageProperties().getConsumerQueue() + this.micrometerHolder.failure(sample, data instanceof Message message + ? message.getMessageProperties().getConsumerQueue() : queuesAsListString(), ex.getClass().getSimpleName()); } Message message; - if (data instanceof Message) { - message = (Message) data; + if (data instanceof Message msg) { + message = msg; } else { message = ((List) data).get(0); @@ -1604,8 +1602,7 @@ private void checkStatefulRetry(RuntimeException ex, Message message) { } private void doExecuteListener(Channel channel, Object data) { - if (data instanceof Message) { - Message message = (Message) data; + if (data instanceof Message message) { if (this.afterReceivePostProcessors != null) { for (MessagePostProcessor processor : this.afterReceivePostProcessors) { message = processor.postProcessMessage(message); @@ -1639,10 +1636,10 @@ protected void invokeListener(Channel channel, Object data) { */ protected void actualInvokeListener(Channel channel, Object data) { Object listener = getMessageListener(); - if (listener instanceof ChannelAwareMessageListener) { - doInvokeListener((ChannelAwareMessageListener) listener, channel, data); + if (listener instanceof ChannelAwareMessageListener chaml) { + doInvokeListener(chaml, channel, data); } - else if (listener instanceof MessageListener) { + else if (listener instanceof MessageListener msgListener) { boolean bindChannel = isExposeListenerChannel() && isChannelLocallyTransacted(); if (bindChannel) { RabbitResourceHolder resourceHolder = new RabbitResourceHolder(channel, false); @@ -1651,7 +1648,7 @@ else if (listener instanceof MessageListener) { resourceHolder); } try { - doInvokeListener((MessageListener) listener, data); + doInvokeListener(msgListener, data); } finally { if (bindChannel) { @@ -2149,8 +2146,7 @@ private static class DefaultExclusiveConsumerLogger implements ConditionalExcept @Override public void log(Log logger, String message, Throwable t) { - if (t instanceof ShutdownSignalException) { - ShutdownSignalException cause = (ShutdownSignalException) t; + if (t instanceof ShutdownSignalException cause) { if (RabbitUtils.isExclusiveUseChannelClose(cause)) { if (logger.isWarnEnabled()) { logger.warn(message + ": " + cause.toString()); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java index 8cac1fc019..a4031e88dc 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java @@ -99,9 +99,9 @@ public abstract class AbstractRabbitListenerEndpoint implements RabbitListenerEn @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanFactory = beanFactory; - if (beanFactory instanceof ConfigurableListableBeanFactory) { - this.resolver = ((ConfigurableListableBeanFactory) beanFactory).getBeanExpressionResolver(); - this.expressionContext = new BeanExpressionContext((ConfigurableListableBeanFactory) beanFactory, null); + if (beanFactory instanceof ConfigurableListableBeanFactory clbf) { + this.resolver = clbf.getBeanExpressionResolver(); + this.expressionContext = new BeanExpressionContext(clbf, null); } this.beanResolver = new BeanFactoryResolver(beanFactory); } @@ -301,6 +301,7 @@ public boolean isBatchListener() { return this.batchListener == null ? false : this.batchListener; } + @Override /** * True if this endpoint is for a batch listener. * @return {@link Boolean#TRUE} if batch. @@ -389,9 +390,7 @@ public void setConverterWinsContentType(boolean converterWinsContentType) { public void setupListenerContainer(MessageListenerContainer listenerContainer) { Collection qNames = getQueueNames(); boolean queueNamesEmpty = qNames.isEmpty(); - if (listenerContainer instanceof AbstractMessageListenerContainer) { - AbstractMessageListenerContainer container = (AbstractMessageListenerContainer) listenerContainer; - + if (listenerContainer instanceof AbstractMessageListenerContainer container) { boolean queuesEmpty = getQueues().isEmpty(); if (!queuesEmpty && !queueNamesEmpty) { throw new IllegalStateException("Queues or queue names must be provided but not both for " + this); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java index e1359b3b39..cf7eacdcf1 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java @@ -739,8 +739,8 @@ private void attemptPassiveDeclarations() { } catch (IllegalArgumentException e) { try { - if (this.channel instanceof ChannelProxy) { - ((ChannelProxy) this.channel).getTargetChannel().close(); + if (this.channel instanceof ChannelProxy proxy) { + proxy.getTargetChannel().close(); } } catch (TimeoutException e1) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ConditionalRejectingErrorHandler.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ConditionalRejectingErrorHandler.java index d58d9918ed..4ddb6c1a5c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ConditionalRejectingErrorHandler.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ConditionalRejectingErrorHandler.java @@ -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. @@ -131,8 +131,8 @@ protected FatalExceptionStrategy getExceptionStrategy() { public void handleError(Throwable t) { log(t); if (!this.causeChainContainsARADRE(t) && this.exceptionStrategy.isFatal(t)) { - if (this.discardFatalsWithXDeath && t instanceof ListenerExecutionFailedException) { - Message failed = ((ListenerExecutionFailedException) t).getFailedMessage(); + if (this.discardFatalsWithXDeath && t instanceof ListenerExecutionFailedException lefe) { + Message failed = lefe.getFailedMessage(); if (failed != null) { List> xDeath = failed.getMessageProperties().getXDeathHeader(); if (xDeath != null && xDeath.size() > 0) { @@ -205,8 +205,8 @@ public boolean isFatal(Throwable t) { && !(cause instanceof MethodArgumentResolutionException)) { cause = cause.getCause(); } - if (t instanceof ListenerExecutionFailedException && isCauseFatal(cause)) { - logFatalException((ListenerExecutionFailedException) t, cause); + if (t instanceof ListenerExecutionFailedException lefe && isCauseFatal(cause)) { + logFatalException(lefe, cause); return true; } return false; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java index 27d26326b3..cabbe13dc6 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java @@ -963,8 +963,8 @@ final class SimpleConsumer extends DefaultConsumer { this.queue = queue; this.index = index; this.ackRequired = !getAcknowledgeMode().isAutoAck() && !getAcknowledgeMode().isManual(); - if (channel instanceof ChannelProxy) { - this.targetChannel = ((ChannelProxy) channel).getTargetChannel(); + if (channel instanceof ChannelProxy proxy) { + this.targetChannel = proxy.getTargetChannel(); } else { this.targetChannel = null; @@ -1050,9 +1050,9 @@ public void handleDelivery(String consumerTag, Envelope envelope, try { executeListenerInTransaction(data, deliveryTag); } - catch (WrappedTransactionException e) { - if (e.getCause() instanceof Error) { - throw (Error) e.getCause(); + catch (WrappedTransactionException ex) { + if (ex.getCause() instanceof Error error) { + throw error; } } catch (Exception e) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistrar.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistrar.java index 998c28957b..99e20ff77b 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistrar.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 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. @@ -188,8 +188,8 @@ protected void registerAllEndpoints() { Assert.state(this.endpointRegistry != null, "No registry available"); synchronized (this.endpointDescriptors) { for (AmqpListenerEndpointDescriptor descriptor : this.endpointDescriptors) { - if (descriptor.endpoint instanceof MultiMethodRabbitListenerEndpoint && this.validator != null) { - ((MultiMethodRabbitListenerEndpoint) descriptor.endpoint).setValidator(this.validator); + if (descriptor.endpoint instanceof MultiMethodRabbitListenerEndpoint multi && this.validator != null) { + multi.setValidator(this.validator); } this.endpointRegistry.registerListenerContainer(// NOSONAR never null descriptor.endpoint, resolveContainerFactory(descriptor)); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistry.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistry.java index 5b20169ffa..7765c3acc9 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistry.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 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. @@ -79,8 +79,8 @@ public class RabbitListenerEndpointRegistry implements DisposableBean, SmartLife @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - if (applicationContext instanceof ConfigurableApplicationContext) { - this.applicationContext = (ConfigurableApplicationContext) applicationContext; + if (applicationContext instanceof ConfigurableApplicationContext configurable) { + this.applicationContext = configurable; } } @@ -209,9 +209,9 @@ public MessageListenerContainer unregisterListenerContainer(String id) { @Override public void destroy() { for (MessageListenerContainer listenerContainer : getListenerContainers()) { - if (listenerContainer instanceof DisposableBean) { + if (listenerContainer instanceof DisposableBean disposable) { try { - ((DisposableBean) listenerContainer).destroy(); + disposable.destroy(); } catch (Exception ex) { this.logger.warn("Failed to destroy listener container [" + listenerContainer + "]", ex); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java index 4bfef6db04..023a7a5031 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java @@ -566,8 +566,8 @@ protected void doStart() { private void checkListenerContainerAware() { Object messageListener = getMessageListener(); - if (messageListener instanceof ListenerContainerAware) { - Collection expectedQueueNames = ((ListenerContainerAware) messageListener).expectedQueueNames(); + if (messageListener instanceof ListenerContainerAware containerAware) { + Collection expectedQueueNames = containerAware.expectedQueueNames(); if (expectedQueueNames != null) { String[] queueNames = getQueueNames(); Assert.state(expectedQueueNames.size() == queueNames.length, diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java index 2e0bd8181e..96b21afb41 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java @@ -374,12 +374,12 @@ protected void handleResult(InvocationResult resultArg, Message request, Channel */ protected void handleResult(InvocationResult resultArg, Message request, Channel channel, Object source) { if (channel != null) { - if (resultArg.getReturnValue() instanceof CompletableFuture) { + if (resultArg.getReturnValue() instanceof CompletableFuture completable) { if (!this.isManualAck) { this.logger.warn("Container AcknowledgeMode must be MANUAL for a Future return type; " + "otherwise the container will ack the message immediately"); } - ((CompletableFuture) resultArg.getReturnValue()).whenComplete((r, t) -> { + completable.whenComplete((r, t) -> { if (t == null) { asyncSuccess(resultArg, request, channel, source, r); basicAck(request, channel); @@ -498,11 +498,13 @@ protected Message buildMessage(Channel channel, Object result, Type genericType) return convert(result, genericType, converter); } else { - if (!(result instanceof Message)) { + if (result instanceof Message msg) { + return msg; + } + else { throw new MessageConversionException("No MessageConverter specified - cannot handle message [" + result + "]"); } - return (Message) result; } } @@ -593,8 +595,8 @@ private Address evaluateReplyTo(Message request, Object source, Object result, E Object value = expression.getValue(this.evalContext, new ReplyExpressionRoot(request, source, result)); Assert.state(value instanceof String || value instanceof Address, "response expression must evaluate to a String or Address"); - if (value instanceof String) { - replyTo = new Address((String) value); + if (value instanceof String sValue) { + replyTo = new Address(sValue); } else { replyTo = (Address) value; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapter.java index d4ef95993a..c9c90a1871 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -269,12 +269,12 @@ public void onMessage(Message message, Channel channel) throws Exception { // NO // In that case, the adapter will simply act as a pass-through. Object delegateListener = getDelegate(); if (!delegateListener.equals(this)) { - if (delegateListener instanceof ChannelAwareMessageListener) { - ((ChannelAwareMessageListener) delegateListener).onMessage(message, channel); + if (delegateListener instanceof ChannelAwareMessageListener chaml) { + chaml.onMessage(message, channel); return; } - else if (delegateListener instanceof MessageListener) { - ((MessageListener) delegateListener).onMessage(message); + else if (delegateListener instanceof MessageListener messageListener) { + messageListener.onMessage(message); return; } } @@ -367,8 +367,8 @@ protected Object invokeListenerMethod(String methodName, Object[] arguments, Mes } catch (InvocationTargetException ex) { Throwable targetEx = ex.getTargetException(); - if (targetEx instanceof IOException) { - throw new AmqpIOException((IOException) targetEx); // NOSONAR lost stack trace + if (targetEx instanceof IOException iox) { + throw new AmqpIOException(iox); // NOSONAR lost stack trace } else { throw new ListenerExecutionFailedException("Listener method '" // NOSONAR lost stack trace diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java index f7acd82e2c..27fa54ead0 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java @@ -300,11 +300,13 @@ protected org.springframework.amqp.core.Message buildMessage(Channel channel, Ob } } else { - if (!(result instanceof org.springframework.amqp.core.Message)) { + if (result instanceof org.springframework.amqp.core.Message msg) { + return msg; + } + else { throw new MessageConversionException("No MessageConverter specified - cannot handle message [" + result + "]"); } - return (org.springframework.amqp.core.Message) result; } } @@ -417,10 +419,8 @@ private Type determineInferredType() { // NOSONAR - complexity } protected Type checkOptional(Type genericParameterType) { - if (genericParameterType instanceof ParameterizedType - && ((ParameterizedType) genericParameterType).getRawType().equals(Optional.class)) { - - return ((ParameterizedType) genericParameterType).getActualTypeArguments()[0]; + if (genericParameterType instanceof ParameterizedType pType && pType.getRawType().equals(Optional.class)) { + return pType.getActualTypeArguments()[0]; } return genericParameterType; } @@ -436,8 +436,7 @@ private boolean isEligibleParameter(MethodParameter methodParameter) { || parameterType.equals(org.springframework.amqp.core.Message.class)) { return false; } - if (parameterType instanceof ParameterizedType) { - ParameterizedType parameterizedType = (ParameterizedType) parameterType; + if (parameterType instanceof ParameterizedType parameterizedType) { if (parameterizedType.getRawType().equals(Message.class)) { return !(parameterizedType.getActualTypeArguments()[0] instanceof WildcardType); } @@ -447,8 +446,7 @@ private boolean isEligibleParameter(MethodParameter methodParameter) { private Type extractGenericParameterTypFromMethodParameter(MethodParameter methodParameter) { Type genericParameterType = methodParameter.getGenericParameterType(); - if (genericParameterType instanceof ParameterizedType) { - ParameterizedType parameterizedType = (ParameterizedType) genericParameterType; + if (genericParameterType instanceof ParameterizedType parameterizedType) { if (parameterizedType.getRawType().equals(Message.class)) { genericParameterType = ((ParameterizedType) genericParameterType).getActualTypeArguments()[0]; } @@ -459,8 +457,8 @@ else if (this.isBatch this.isCollection = true; Type paramType = parameterizedType.getActualTypeArguments()[0]; - boolean messageHasGeneric = paramType instanceof ParameterizedType - && ((ParameterizedType) paramType).getRawType().equals(Message.class); + boolean messageHasGeneric = paramType instanceof ParameterizedType pType + && pType.getRawType().equals(Message.class); this.isMessageList = paramType.equals(Message.class) || messageHasGeneric; this.isAmqpMessageList = paramType.equals(org.springframework.amqp.core.Message.class); if (messageHasGeneric) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/support/ContainerUtils.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/support/ContainerUtils.java index cf88726675..06d83ebc02 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/support/ContainerUtils.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/support/ContainerUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-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. @@ -75,8 +75,8 @@ else if (t instanceof ImmediateRequeueAmqpException) { * @since 2.2 */ public static boolean isRejectManual(Throwable ex) { - return ex instanceof AmqpRejectAndDontRequeueException - && ((AmqpRejectAndDontRequeueException) ex).isRejectManual(); + return ex instanceof AmqpRejectAndDontRequeueException aradrex + && aradrex.isRejectManual(); } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/AmqpAppender.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/AmqpAppender.java index be35f98c0b..ba5a6e25f9 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/AmqpAppender.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/AmqpAppender.java @@ -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. @@ -952,10 +952,10 @@ private void sendOneEncoderPatternMessage(RabbitTemplate rabbitTemplate, String private void doSend(RabbitTemplate rabbitTemplate, final Event event, ILoggingEvent logEvent, String name, MessageProperties amqpProps, String routingKey) { byte[] msgBody; - if (AmqpAppender.this.abbreviator != null && logEvent instanceof LoggingEvent) { - ((LoggingEvent) logEvent).setLoggerName(AmqpAppender.this.abbreviator.abbreviate(name)); + if (AmqpAppender.this.abbreviator != null && logEvent instanceof LoggingEvent logEv) { + logEv.setLoggerName(AmqpAppender.this.abbreviator.abbreviate(name)); msgBody = encodeMessage(logEvent); - ((LoggingEvent) logEvent).setLoggerName(name); + logEv.setLoggerName(name); } else { msgBody = encodeMessage(logEvent); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java index 4fb2d0c73f..72053d2846 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -92,8 +92,8 @@ public MessageProperties toMessageProperties(final BasicProperties source, final String key = entry.getKey(); if (MessageProperties.X_DELAY.equals(key)) { Object value = entry.getValue(); - if (value instanceof Integer) { - target.setReceivedDelay((Integer) value); + if (value instanceof Integer integ) { + target.setReceivedDelay(integ); } } else { @@ -192,8 +192,7 @@ private Object convertHeaderValueIfNecessary(@Nullable Object valueArg) { if (!valid && value != null) { value = value.toString(); } - else if (value instanceof Object[]) { - Object[] array = (Object[]) value; + else if (value instanceof Object[] array) { Object[] writableArray = new Object[array.length]; for (int i = 0; i < writableArray.length; i++) { writableArray[i] = convertHeaderValueIfNecessary(array[i]); @@ -216,8 +215,8 @@ else if (value instanceof Map) { } value = writableMap; } - else if (value instanceof Class) { - value = ((Class) value).getName(); + else if (value instanceof Class clazz) { + value = clazz.getName(); } return value; } @@ -254,8 +253,8 @@ private Object convertLongString(LongString longString, String charset) { */ private Object convertLongStringIfNecessary(Object valueArg, String charset) { Object value = valueArg; - if (value instanceof LongString) { - value = convertLongString((LongString) value, charset); + if (value instanceof LongString longStr) { + value = convertLongString(longStr, charset); } else if (value instanceof List) { List convertedList = new ArrayList(((List) value).size()); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/RabbitExceptionTranslator.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/RabbitExceptionTranslator.java index 19e339bb83..d553473dc0 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/RabbitExceptionTranslator.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/RabbitExceptionTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -54,11 +54,11 @@ public static RuntimeException convertRabbitAccessException(Throwable ex) { if (ex instanceof AmqpException) { return (AmqpException) ex; } - if (ex instanceof ShutdownSignalException) { - return new AmqpConnectException((ShutdownSignalException) ex); + if (ex instanceof ShutdownSignalException sigEx) { + return new AmqpConnectException(sigEx); } - if (ex instanceof ConnectException) { - return new AmqpConnectException((ConnectException) ex); + if (ex instanceof ConnectException connEx) { + return new AmqpConnectException(connEx); } if (ex instanceof PossibleAuthenticationFailureException) { return new AmqpAuthenticationException(ex); @@ -66,8 +66,8 @@ public static RuntimeException convertRabbitAccessException(Throwable ex) { if (ex instanceof UnsupportedEncodingException) { return new AmqpUnsupportedEncodingException(ex); } - if (ex instanceof IOException) { - return new AmqpIOException((IOException) ex); + if (ex instanceof IOException ioEx) { + return new AmqpIOException(ioEx); } if (ex instanceof TimeoutException) { return new AmqpTimeoutException(ex); From 8045f2c8661e2abdcd0955266543de33fa18ab36 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 12 Oct 2022 14:53:00 -0400 Subject: [PATCH 168/737] GH-1419: Fix Early Exit in NodeLocator If a node was returned by the REST call and the node was not in the map of nodes to addreses, the loop exited early. The incorrect variable was being tested (never null). Also add a more sophisticated integration test - using 2 brokers, ensure that the correct broker is located for the queue. --- build.gradle | 5 +- .../junit/AbstractTestContainerTests.java | 15 ++-- .../config/SuperStreamProvisioningTests.java | 4 +- .../stream/listener/RabbitListenerTests.java | 4 +- .../SuperStreamConcurrentSACTests.java | 4 +- .../stream/listener/SuperStreamSACTests.java | 4 +- .../LocalizedQueueConnectionFactory.java | 2 +- ...ueueConnectionFactoryIntegrationTests.java | 83 +++++++++++++++---- .../rabbit/connection/NodeLocatorTests.java | 69 +++++++++++++++ 9 files changed, 158 insertions(+), 32 deletions(-) rename spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/AbstractIntegrationTests.java => spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/AbstractTestContainerTests.java (81%) create mode 100644 spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/NodeLocatorTests.java diff --git a/build.gradle b/build.gradle index 2684b30b7c..e42d3f4964 100644 --- a/build.gradle +++ b/build.gradle @@ -67,6 +67,7 @@ ext { springDataVersion = '2022.0.0-SNAPSHOT' springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.0-SNAPSHOT' springRetryVersion = '2.0.0-SNAPSHOT' + testContainersVersion = '1.17.3' zstdJniVersion = '1.5.0-2' } @@ -410,6 +411,7 @@ project('spring-rabbit') { testImplementation 'io.micrometer:micrometer-tracing-bridge-brave' testImplementation 'io.micrometer:micrometer-tracing-test' testImplementation 'io.micrometer:micrometer-tracing-integration-test' + testImplementation "org.testcontainers:rabbitmq:$testContainersVersion" testRuntimeOnly 'com.fasterxml.jackson.core:jackson-core' testRuntimeOnly 'com.fasterxml.jackson.core:jackson-databind' testRuntimeOnly 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' @@ -474,7 +476,7 @@ project('spring-rabbit-stream') { testRuntimeOnly "org.xerial.snappy:snappy-java:$snappyVersion" testRuntimeOnly "org.lz4:lz4-java:$lz4Version" testRuntimeOnly "com.github.luben:zstd-jni:$zstdJniVersion" - testImplementation "org.testcontainers:rabbitmq:1.17.3" + testImplementation "org.testcontainers:rabbitmq:$testContainersVersion" testImplementation "org.apache.logging.log4j:log4j-slf4j-impl:$log4jVersion" testImplementation 'org.springframework:spring-webflux' } @@ -495,6 +497,7 @@ project('spring-rabbit-junit') { api 'org.springframework:spring-web' api 'org.junit.jupiter:junit-jupiter-api' api "org.assertj:assertj-core:$assertjVersion" + optionalApi "org.testcontainers:rabbitmq:$testContainersVersion" optionalApi "ch.qos.logback:logback-classic:$logbackVersion" optionalApi 'org.apache.logging.log4j:log4j-core' compileOnly 'org.apiguardian:apiguardian-api:1.0.0' diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/AbstractIntegrationTests.java b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/AbstractTestContainerTests.java similarity index 81% rename from spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/AbstractIntegrationTests.java rename to spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/AbstractTestContainerTests.java index 9b6f1575d5..7504a46595 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/AbstractIntegrationTests.java +++ b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/AbstractTestContainerTests.java @@ -14,11 +14,10 @@ * limitations under the License. */ -package org.springframework.rabbit.stream.support; +package org.springframework.amqp.rabbit.junit; import java.time.Duration; -import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.RabbitMQContainer; /** @@ -26,9 +25,9 @@ * @since 2.4 * */ -public abstract class AbstractIntegrationTests { +public abstract class AbstractTestContainerTests { - static final GenericContainer RABBITMQ; + protected static final RabbitMQContainer RABBITMQ; static { if (System.getProperty("spring.rabbit.use.local.server") == null @@ -40,7 +39,7 @@ public abstract class AbstractIntegrationTests { } RABBITMQ = new RabbitMQContainer(image) .withExposedPorts(5672, 15672, 5552) - .withPluginsEnabled("rabbitmq_stream", "rabbitmq_management") + .withPluginsEnabled("rabbitmq_stream") .withStartupTimeout(Duration.ofMinutes(2)); RABBITMQ.start(); } @@ -50,7 +49,7 @@ public abstract class AbstractIntegrationTests { } public static int amqpPort() { - return RABBITMQ != null ? RABBITMQ.getMappedPort(5672) : 5672; + return RABBITMQ != null ? RABBITMQ.getAmqpPort() : 5672; } public static int managementPort() { @@ -61,4 +60,8 @@ public static int streamPort() { return RABBITMQ != null ? RABBITMQ.getMappedPort(5552) : 5552; } + public static String restUri() { + return RABBITMQ.getHttpUrl() + "/api/"; + } + } diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamProvisioningTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamProvisioningTests.java index b96eb88125..b87daf8db8 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamProvisioningTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamProvisioningTests.java @@ -28,10 +28,10 @@ import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.junit.AbstractTestContainerTests; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.rabbit.stream.support.AbstractIntegrationTests; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; /** @@ -40,7 +40,7 @@ * */ @SpringJUnitConfig -public class SuperStreamProvisioningTests extends AbstractIntegrationTests { +public class SuperStreamProvisioningTests extends AbstractTestContainerTests { @Test void provision(@Autowired Declarables declarables, @Autowired CachingConnectionFactory cf, diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java index ba4bd3e534..0ea7fc50ea 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java @@ -40,6 +40,7 @@ import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitAdmin; import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.junit.AbstractTestContainerTests; import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.SmartLifecycle; @@ -51,7 +52,6 @@ import org.springframework.rabbit.stream.config.StreamRabbitListenerContainerFactory; import org.springframework.rabbit.stream.producer.RabbitStreamTemplate; import org.springframework.rabbit.stream.retry.StreamRetryOperationsInterceptorFactoryBean; -import org.springframework.rabbit.stream.support.AbstractIntegrationTests; import org.springframework.rabbit.stream.support.StreamMessageProperties; import org.springframework.retry.interceptor.RetryOperationsInterceptor; import org.springframework.test.annotation.DirtiesContext; @@ -73,7 +73,7 @@ */ @SpringJUnitConfig @DirtiesContext -public class RabbitListenerTests extends AbstractIntegrationTests { +public class RabbitListenerTests extends AbstractTestContainerTests { @Autowired Config config; diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamConcurrentSACTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamConcurrentSACTests.java index f089a2ca8b..40e7fdbada 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamConcurrentSACTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamConcurrentSACTests.java @@ -32,11 +32,11 @@ import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitAdmin; import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.junit.AbstractTestContainerTests; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.rabbit.stream.config.SuperStream; -import org.springframework.rabbit.stream.support.AbstractIntegrationTests; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.rabbitmq.stream.Address; @@ -49,7 +49,7 @@ * */ @SpringJUnitConfig -public class SuperStreamConcurrentSACTests extends AbstractIntegrationTests { +public class SuperStreamConcurrentSACTests extends AbstractTestContainerTests { @Test void concurrent(@Autowired StreamListenerContainer container, @Autowired RabbitTemplate template, diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java index 2dd1a4614e..595e18cb5b 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java @@ -37,6 +37,7 @@ import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitAdmin; import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.junit.AbstractTestContainerTests; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.ApplicationContext; @@ -44,7 +45,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; import org.springframework.rabbit.stream.config.SuperStream; -import org.springframework.rabbit.stream.support.AbstractIntegrationTests; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.rabbitmq.stream.Address; @@ -57,7 +57,7 @@ * */ @SpringJUnitConfig -public class SuperStreamSACTests extends AbstractIntegrationTests { +public class SuperStreamSACTests extends AbstractTestContainerTests { @Test void superStream(@Autowired ApplicationContext context, @Autowired RabbitTemplate template, diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java index aa25c7a5ff..ab96518bee 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java @@ -398,7 +398,7 @@ default ConnectionFactory locate(String[] adminUris, Map nodeToA String node = (String) queueInfo.get("node"); if (node != null) { String nodeUri = nodeToAddress.get(node); - if (uri != null) { + if (nodeUri != null) { close(client); return factoryFunction.locate(queue, node, nodeUri); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryIntegrationTests.java index 9c2a2dd7d2..b641bb69e2 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryIntegrationTests.java @@ -19,17 +19,25 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.Map; -import java.util.UUID; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.AnonymousQueue; import org.springframework.amqp.rabbit.core.RabbitAdmin; -import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.junit.AbstractTestContainerTests; import org.springframework.amqp.rabbit.junit.RabbitAvailable; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.MediaType; +import org.springframework.web.reactive.function.client.ExchangeFilterFunctions; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.UriUtils; /** @@ -37,18 +45,27 @@ * @author Gary Russell */ @RabbitAvailable(management = true, queues = "local") -public class LocalizedQueueConnectionFactoryIntegrationTests { +public class LocalizedQueueConnectionFactoryIntegrationTests extends AbstractTestContainerTests { private LocalizedQueueConnectionFactory lqcf; private CachingConnectionFactory defaultConnectionFactory; + private CachingConnectionFactory testContainerFactory; + + private RabbitAdmin defaultAdmin; + + private RabbitAdmin testContainerAdmin; + @BeforeEach public void setup() { this.defaultConnectionFactory = new CachingConnectionFactory("localhost"); - String[] addresses = new String[] { "localhost:9999", "localhost:5672" }; - String[] adminUris = new String[] { "http://localhost:15672", "http://localhost:15672" }; - String[] nodes = new String[] { "foo@bar", "rabbit@localhost" }; + this.defaultAdmin = new RabbitAdmin(this.defaultConnectionFactory); + this.testContainerFactory = new CachingConnectionFactory("localhost", amqpPort()); + this.testContainerAdmin = new RabbitAdmin(this.testContainerFactory); + String[] addresses = new String[] { "localhost:5672", "localhost:" + amqpPort() }; + String[] adminUris = new String[] { "http://localhost:15672", "http://localhost:" + managementPort() }; + String[] nodes = new String[] { "rabbit@localhost", findTcNode() }; String vhost = "/"; String username = "guest"; String password = "guest"; @@ -60,18 +77,24 @@ public void setup() { public void tearDown() { this.lqcf.destroy(); this.defaultConnectionFactory.destroy(); + this.testContainerFactory.destroy(); } @Test - public void testConnect() throws Exception { - RabbitAdmin admin = new RabbitAdmin(this.lqcf); - Queue queue = new Queue(UUID.randomUUID().toString(), false, false, true); - admin.declareQueue(queue); - ConnectionFactory targetConnectionFactory = this.lqcf.getTargetConnectionFactory("[" + queue.getName() + "]"); - RabbitTemplate template = new RabbitTemplate(targetConnectionFactory); - template.convertAndSend("", queue.getName(), "foo"); - assertThat(template.receiveAndConvert(queue.getName())).isEqualTo("foo"); - admin.deleteQueue(queue.getName()); + public void testFindCorrectConnection() throws Exception { + AnonymousQueue externalQueue = new AnonymousQueue(); + AnonymousQueue tcQueue = new AnonymousQueue(); + this.defaultAdmin.declareQueue(externalQueue); + this.testContainerAdmin.declareQueue(tcQueue); + ConnectionFactory cf = this.lqcf + .getTargetConnectionFactory("[" + externalQueue.getName() + "]"); + assertThat(cf).isNotSameAs(this.defaultConnectionFactory); + assertThat(this.defaultAdmin.getQueueProperties(externalQueue.getName())).isNotNull(); + cf = this.lqcf.getTargetConnectionFactory("[" + tcQueue.getName() + "]"); + assertThat(cf).isNotSameAs(this.defaultConnectionFactory); + assertThat(this.testContainerAdmin.getQueueProperties(tcQueue.getName())).isNotNull(); + this.defaultAdmin.deleteQueue(externalQueue.getName()); + this.testContainerAdmin.deleteQueue(tcQueue.getName()); } @Test @@ -89,4 +112,32 @@ void findLocal() { lqcf.destroy(); } + private String findTcNode() { + AnonymousQueue queue = new AnonymousQueue(); + this.testContainerAdmin.declareQueue(queue); + URI uri; + try { + uri = new URI(restUri()) + .resolve("/api/queues/" + UriUtils.encodePathSegment("/", StandardCharsets.UTF_8) + "/" + + queue.getName()); + } + catch (URISyntaxException ex) { + throw new IllegalStateException(ex); + } + WebClient client = WebClient.builder() + .filter(ExchangeFilterFunctions.basicAuthentication(RABBITMQ.getAdminUsername(), + RABBITMQ.getAdminPassword())) + .build(); + Map queueInfo = client.get() + .uri(uri) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + }) + .block(Duration.ofSeconds(10)); + this.testContainerAdmin.deleteQueue(queue.getName()); + return (String) queueInfo.get("node"); + } + + } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/NodeLocatorTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/NodeLocatorTests.java new file mode 100644 index 0000000000..b05efe28fe --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/NodeLocatorTests.java @@ -0,0 +1,69 @@ +/* + * 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.connection; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.net.URISyntaxException; +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.rabbit.connection.LocalizedQueueConnectionFactory.NodeLocator; +import org.springframework.lang.Nullable; + +/** + * @author Gary Russell + * @since 3.0 + * + */ +public class NodeLocatorTests { + + @Test + @DisplayName("don't exit early when node to address missing") + void missingNode() throws URISyntaxException { + + NodeLocator nodeLocator = spy(new NodeLocator() { + + @Override + public Object createClient(String userName, String password) { + return null; + } + + @Override + @Nullable + public Map restCall(Object client, String baseUri, String vhost, String queue) { + if (baseUri.contains("foo")) { + return Map.of("node", "c@d"); + } + else { + return Map.of("node", "a@b"); + } + } + }); + ConnectionFactory factory = nodeLocator.locate(new String[] { "http://foo", "http://bar" }, + Map.of("a@b", "baz"), null, "q", null, null, (q, n, u) -> { + return null; + }); + verify(nodeLocator, times(2)).restCall(any(), any(), any(), any()); + } + +} From 4927cc58a172eb16703e31e73c3576bff942c28d Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Fri, 14 Oct 2022 09:03:39 -0400 Subject: [PATCH 169/737] GH-1419: Sonar Fixes --- .../amqp/rabbit/junit/BrokerRunningSupport.java | 15 +++++++++------ .../rabbit/connection/WebFluxNodeLocator.java | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java index 9caf727a57..cb7d9a652e 100644 --- a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java +++ b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java @@ -38,6 +38,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.http.HttpStatus; import org.springframework.util.Base64Utils; import org.springframework.util.StringUtils; import org.springframework.web.util.UriUtils; @@ -379,11 +380,9 @@ private Channel createQueues(Connection connection) throws IOException, URISynta channel.queueDeclare(queueName, true, false, false, null); } } - if (this.management) { - if (!alivenessTest()) { - throw new BrokerNotAliveException("Aliveness test failed for localhost:15672 guest/quest; " - + "management not available"); - } + if (this.management && !alivenessTest()) { + throw new BrokerNotAliveException("Aliveness test failed for localhost:15672 guest/quest; " + + "management not available"); } return channel; } @@ -417,7 +416,11 @@ protected PasswordAuthentication getPasswordAuthentication() { Thread.currentThread().interrupt(); return false; } - return response.body().contentEquals("{\"status\":\"ok\"}"); + String body = null; + if (response.statusCode() == HttpStatus.OK.value()) { + response.body(); + } + return body != null && body.contentEquals("{\"status\":\"ok\"}"); } public static boolean fatal() { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/WebFluxNodeLocator.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/WebFluxNodeLocator.java index 5179ea378a..5e00db4ae9 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/WebFluxNodeLocator.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/WebFluxNodeLocator.java @@ -54,7 +54,7 @@ public Map restCall(WebClient client, String baseUri, String vho .bodyToMono(new ParameterizedTypeReference>() { }) .block(Duration.ofSeconds(10)); // NOSONAR magic# - return queueInfo; + return queueInfo != null ? queueInfo : null; } /** From 7ddee21130a9d07a48b91445b49e4bb692389166 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Fri, 14 Oct 2022 09:46:18 -0400 Subject: [PATCH 170/737] Improve Management Plugin Test Include diagnostics when not alive. --- .../rabbit/junit/BrokerRunningSupport.java | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java index cb7d9a652e..544b9c480b 100644 --- a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java +++ b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java @@ -380,14 +380,13 @@ private Channel createQueues(Connection connection) throws IOException, URISynta channel.queueDeclare(queueName, true, false, false, null); } } - if (this.management && !alivenessTest()) { - throw new BrokerNotAliveException("Aliveness test failed for localhost:15672 guest/quest; " - + "management not available"); + if (this.management) { + alivenessTest(); } return channel; } - private boolean alivenessTest() throws URISyntaxException { + private void alivenessTest() throws URISyntaxException { HttpClient client = HttpClient.newBuilder() .authenticator(new Authenticator() { @@ -409,18 +408,22 @@ protected PasswordAuthentication getPasswordAuthentication() { response = client.send(request, BodyHandlers.ofString()); } catch (IOException ex) { - LOGGER.error("Exception checking admin aliveness", ex); - return false; + throw new BrokerNotAliveException("Failed to check aliveness", ex); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); - return false; + throw new BrokerNotAliveException("Interrupted while checking aliveness", ex); } String body = null; if (response.statusCode() == HttpStatus.OK.value()) { - response.body(); + body = response.body(); + } + if (body == null || !body.contentEquals("{\"status\":\"ok\"}")) { + throw new BrokerNotAliveException("Aliveness test failed for " + uri.toString() + + " user: " + getAdminUser() + " pw: " + getAdminPassword() + + " status: " + response.statusCode() + " body: " + body + + "; management not available"); } - return body != null && body.contentEquals("{\"status\":\"ok\"}"); } public static boolean fatal() { From 82c69d535895c512e5924224c52ed7006146a77c Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Fri, 14 Oct 2022 10:45:22 -0400 Subject: [PATCH 171/737] GH-1419: Fix Local Node Name in Tests Local node not always `rabbit@localhost`; can be `rabbit@realHostName`. --- ...ueueConnectionFactoryIntegrationTests.java | 48 ++++++++++++++++--- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryIntegrationTests.java index b641bb69e2..a82307863f 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryIntegrationTests.java @@ -32,7 +32,9 @@ import org.springframework.amqp.core.AnonymousQueue; import org.springframework.amqp.rabbit.core.RabbitAdmin; import org.springframework.amqp.rabbit.junit.AbstractTestContainerTests; +import org.springframework.amqp.rabbit.junit.BrokerRunningSupport; import org.springframework.amqp.rabbit.junit.RabbitAvailable; +import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.MediaType; import org.springframework.web.reactive.function.client.ExchangeFilterFunctions; @@ -63,12 +65,15 @@ public void setup() { this.defaultAdmin = new RabbitAdmin(this.defaultConnectionFactory); this.testContainerFactory = new CachingConnectionFactory("localhost", amqpPort()); this.testContainerAdmin = new RabbitAdmin(this.testContainerFactory); - String[] addresses = new String[] { "localhost:5672", "localhost:" + amqpPort() }; - String[] adminUris = new String[] { "http://localhost:15672", "http://localhost:" + managementPort() }; - String[] nodes = new String[] { "rabbit@localhost", findTcNode() }; + BrokerRunningSupport brokerRunning = RabbitAvailableCondition.getBrokerRunning(); + + String[] addresses = new String[] { brokerRunning.getHostName() + ":" + brokerRunning.getPort(), + RABBITMQ.getHost() + ":" + RABBITMQ.getAmqpPort() }; + String[] adminUris = new String[] { brokerRunning.getAdminUri(), RABBITMQ.getHttpUrl() }; + String[] nodes = new String[] { findLocalNode(), findTcNode() }; String vhost = "/"; - String username = "guest"; - String password = "guest"; + String username = brokerRunning.getAdminUser(); + String password = brokerRunning.getAdminPassword(); this.lqcf = new LocalizedQueueConnectionFactory(defaultConnectionFactory, addresses, adminUris, nodes, vhost, username, password, false, null); } @@ -100,9 +105,11 @@ public void testFindCorrectConnection() throws Exception { @Test void findLocal() { ConnectionFactory defaultCf = mock(ConnectionFactory.class); + BrokerRunningSupport brokerRunning = RabbitAvailableCondition.getBrokerRunning(); LocalizedQueueConnectionFactory lqcf = new LocalizedQueueConnectionFactory(defaultCf, - Map.of("rabbit@localhost", "localhost:5672"), new String[] { "http://localhost:15672" }, - "/", "guest", "guest", false, null); + Map.of(findLocalNode(), brokerRunning.getHostName() + ":" + brokerRunning.getPort()), + new String[] { brokerRunning.getAdminUri() }, + "/", brokerRunning.getAdminUser(), brokerRunning.getAdminPassword(), false, null); ConnectionFactory cf = lqcf.getTargetConnectionFactory("[local]"); RabbitAdmin admin = new RabbitAdmin(cf); assertThat(admin.getQueueProperties("local")).isNotNull(); @@ -139,5 +146,32 @@ private String findTcNode() { return (String) queueInfo.get("node"); } + private String findLocalNode() { + AnonymousQueue queue = new AnonymousQueue(); + this.defaultAdmin.declareQueue(queue); + URI uri; + BrokerRunningSupport brokerRunning = RabbitAvailableCondition.getBrokerRunning(); + try { + uri = new URI(brokerRunning.getAdminUri()) + .resolve("/api/queues/" + UriUtils.encodePathSegment("/", StandardCharsets.UTF_8) + "/" + + queue.getName()); + } + catch (URISyntaxException ex) { + throw new IllegalStateException(ex); + } + WebClient client = WebClient.builder() + .filter(ExchangeFilterFunctions.basicAuthentication(brokerRunning.getAdminUser(), + brokerRunning.getAdminPassword())) + .build(); + Map queueInfo = client.get() + .uri(uri) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + }) + .block(Duration.ofSeconds(10)); + this.defaultAdmin.deleteQueue(queue.getName()); + return (String) queueInfo.get("node"); + } } From 6e23963c3e05d1cf624a7823004556a45770127f Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Fri, 14 Oct 2022 11:25:08 -0400 Subject: [PATCH 172/737] GH-1419: Increase New Code Test Coverage --- .../rabbit/connection/NodeLocatorTests.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/NodeLocatorTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/NodeLocatorTests.java index b05efe28fe..0fe98fb487 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/NodeLocatorTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/NodeLocatorTests.java @@ -58,6 +58,62 @@ public Map restCall(Object client, String baseUri, String vhost, return Map.of("node", "a@b"); } } + + }); + ConnectionFactory factory = nodeLocator.locate(new String[] { "http://foo", "http://bar" }, + Map.of("a@b", "baz"), null, "q", null, null, (q, n, u) -> { + return null; + }); + verify(nodeLocator, times(2)).restCall(any(), any(), any(), any()); + } + + @Test + @DisplayName("rest returned null") + void nullInfo() throws URISyntaxException { + + NodeLocator nodeLocator = spy(new NodeLocator() { + + @Override + public Object createClient(String userName, String password) { + return null; + } + + @Override + @Nullable + public Map restCall(Object client, String baseUri, String vhost, String queue) { + if (baseUri.contains("foo")) { + return null; + } + else { + return Map.of("node", "a@b"); + } + } + + }); + ConnectionFactory factory = nodeLocator.locate(new String[] { "http://foo", "http://bar" }, + Map.of("a@b", "baz"), null, "q", null, null, (q, n, u) -> { + return null; + }); + verify(nodeLocator, times(2)).restCall(any(), any(), any(), any()); + } + + @Test + @DisplayName("queue not found") + void notFound() throws URISyntaxException { + + NodeLocator nodeLocator = spy(new NodeLocator() { + + @Override + public Object createClient(String userName, String password) { + return null; + } + + @Override + @Nullable + public Map restCall(Object client, String baseUri, String vhost, String queue) { + return null; + } + }); ConnectionFactory factory = nodeLocator.locate(new String[] { "http://foo", "http://bar" }, Map.of("a@b", "baz"), null, "q", null, null, (q, n, u) -> { From ea0daee219e54dccd615c358dc990b9a35ae0079 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 17 Oct 2022 10:05:03 -0400 Subject: [PATCH 173/737] Upgrade Versions; Prepare for Release --- build.gradle | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/build.gradle b/build.gradle index e42d3f4964..5d9c595c44 100644 --- a/build.gradle +++ b/build.gradle @@ -48,25 +48,25 @@ ext { commonsPoolVersion = '2.11.1' googleJsr305Version = '3.0.2' hamcrestVersion = '2.2' - hibernateValidationVersion = '7.0.4.Final' - jacksonBomVersion = '2.13.4' - jaywayJsonPathVersion = '2.6.0' + hibernateValidationVersion = '7.0.5.Final' + jacksonBomVersion = '2.13.4.20221013' + jaywayJsonPathVersion = '2.7.0' junit4Version = '4.13.2' - junitJupiterVersion = '5.9.0' - log4jVersion = '2.18.0' - logbackVersion = '1.2.3' + junitJupiterVersion = '5.9.1' + log4jVersion = '2.19.0' + logbackVersion = '1.4.4' lz4Version = '1.8.0' - micrometerDocsVersion = '1.0.0-SNAPSHOT' - micrometerVersion = '1.10.0-SNAPSHOT' - micrometerTracingVersion = '1.0.0-SNAPSHOT' + micrometerDocsVersion = '1.0.0-RC1' + micrometerVersion = '1.10.0-RC1' + micrometerTracingVersion = '1.0.0-RC1' mockitoVersion = '4.8.0' rabbitmqStreamVersion = '0.8.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.16.0' - reactorVersion = '2022.0.0-SNAPSHOT' + reactorVersion = '2022.0.0-RC1' snappyVersion = '1.1.8.4' - springDataVersion = '2022.0.0-SNAPSHOT' - springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.0-SNAPSHOT' - springRetryVersion = '2.0.0-SNAPSHOT' + springDataVersion = '2022.0.0-RC1' + springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.0-RC1' + springRetryVersion = '2.0.0-RC1' testContainersVersion = '1.17.3' zstdJniVersion = '1.5.0-2' } From 9f8de0e9af575b969269cad047c9b6b37ea2f783 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 17 Oct 2022 14:47:53 +0000 Subject: [PATCH 174/737] [artifactory-release] Release version 3.0.0-RC1 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 2477c6a3c5..2f5f2b6815 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.0-SNAPSHOT +version=3.0.0-RC1 org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true From 34067d24280a705f4837b95b934d00152e1c768b Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 17 Oct 2022 14:47:55 +0000 Subject: [PATCH 175/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 2f5f2b6815..2477c6a3c5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.0-RC1 +version=3.0.0-SNAPSHOT org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true From aa86a0292dfca60d468ae15b2b2f946d2e0259c7 Mon Sep 17 00:00:00 2001 From: felgentraeger Date: Mon, 17 Oct 2022 09:35:36 +0200 Subject: [PATCH 176/737] GH-1517: Add CompositeContainerCustomizer Resolves https://github.com/spring-projects/spring-amqp/issues/1517 Add configuration support for multiple ContainerCustomizer at once --- .../config/CompositeContainerCustomizer.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizer.java diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizer.java new file mode 100644 index 0000000000..57e762e781 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizer.java @@ -0,0 +1,23 @@ +package org.springframework.amqp.rabbit.config; + +import java.util.List; +import org.springframework.amqp.rabbit.listener.MessageListenerContainer; +import org.springframework.util.Assert; + +/** + * Implementation of {@link ContainerCustomizer} providing the configuration of multiple customizers at the same time. + */ +public class CompositeContainerCustomizer implements ContainerCustomizer { + + private final List> customizers; + + public CompositeContainerCustomizer(List> customizers) { + Assert.notNull(customizers, "At least one customizer must be present"); + this.customizers = customizers; + } + + @Override + public void configure(C container) { + customizers.forEach(c -> c.configure(container)); + } +} From b9b83f9bf89a05939b62b90b61116a372b5626a8 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 18 Oct 2022 09:33:43 -0400 Subject: [PATCH 177/737] GH-1517: Docs and Polishing for Composite Cust. --- .../config/CompositeContainerCustomizer.java | 35 ++++++++++++-- .../CompositeContainerCustomizerTests.java | 48 +++++++++++++++++++ src/reference/asciidoc/amqp.adoc | 2 + 3 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizerTests.java diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizer.java index 57e762e781..31c77171ba 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizer.java @@ -1,23 +1,52 @@ +/* + * 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.config; +import java.util.ArrayList; import java.util.List; + import org.springframework.amqp.rabbit.listener.MessageListenerContainer; import org.springframework.util.Assert; /** - * Implementation of {@link ContainerCustomizer} providing the configuration of multiple customizers at the same time. + * Implementation of {@link ContainerCustomizer} providing the configuration of + * multiple customizers at the same time. + * + * @param the container type. + * + * @author Rene Felgentraeger + * @author Gary Russell */ public class CompositeContainerCustomizer implements ContainerCustomizer { - private final List> customizers; + private final List> customizers = new ArrayList<>(); + /** + * Create an instance with the provided delegate customizers. + * @param customizers the customizers. + */ public CompositeContainerCustomizer(List> customizers) { Assert.notNull(customizers, "At least one customizer must be present"); - this.customizers = customizers; + this.customizers.addAll(customizers); } @Override public void configure(C container) { customizers.forEach(c -> c.configure(container)); } + } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizerTests.java new file mode 100644 index 0000000000..3162721c76 --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizerTests.java @@ -0,0 +1,48 @@ +/* + * 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.config; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.rabbit.listener.MessageListenerContainer; + +/** + * @author Gary Russell + * @since 2.4.8 + * + */ +public class CompositeContainerCustomizerTests { + + @SuppressWarnings("unchecked") + @Test + void allCalled() { + ContainerCustomizer mock1 = mock(ContainerCustomizer.class); + ContainerCustomizer mock2 = mock(ContainerCustomizer.class); + CompositeContainerCustomizer cust = new CompositeContainerCustomizer<>( + List.of(mock1, mock2)); + MessageListenerContainer mlc = mock(MessageListenerContainer.class); + cust.configure(mlc); + verify(mock1).configure(mlc); + verify(mock2).configure(mlc); + } + +} diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index 36ea2bcacc..67a3d8511e 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -2598,6 +2598,8 @@ NOTE: For information to help you choose between `SimpleRabbitListenerContainerF Starting wih version 2.2.2, you can provide a `ContainerCustomizer` implementation (as shown above). This can be used to further configure the container after it has been created and configured; you can use this, for example, to set properties that are not exposed by the container factory. +Version 2.4.8 provides the `CompositeContainerCustomizer` for situations where you wish to apply multiple customizers. + 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. From 0bc387f919ce157566dd51e336d950c9f6cc806d Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 18 Oct 2022 09:48:21 -0400 Subject: [PATCH 178/737] GH-1517: Add Since Tag --- .../amqp/rabbit/config/CompositeContainerCustomizer.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizer.java index 31c77171ba..d16b548912 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizer.java @@ -30,6 +30,8 @@ * * @author Rene Felgentraeger * @author Gary Russell + * + * @since 2.4.8 */ public class CompositeContainerCustomizer implements ContainerCustomizer { From 2a4a0ecb6b5e0bbd40d600b53cc0e194bf92e401 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 18 Oct 2022 13:18:47 -0400 Subject: [PATCH 179/737] GH-1517: Fix Javadoc, CheckStyle --- build.gradle | 2 +- .../amqp/rabbit/config/CompositeContainerCustomizer.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 5d9c595c44..4f2f21bcce 100644 --- a/build.gradle +++ b/build.gradle @@ -198,7 +198,7 @@ subprojects { subproject -> } // enable all compiler warnings; individual projects may customize further - ext.xLintArg = '-Xlint:all,-options,-processing' + ext.xLintArg = '-Xlint:all,-options,-processing,-deprecation' [compileJava, compileTestJava]*.options*.compilerArgs = [xLintArg] publishing { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizer.java index d16b548912..1ec652c818 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizer.java @@ -23,7 +23,7 @@ import org.springframework.util.Assert; /** - * Implementation of {@link ContainerCustomizer} providing the configuration of + * Implementation of {@link ContainerCustomizer} providing the configuration of * multiple customizers at the same time. * * @param the container type. @@ -48,7 +48,7 @@ public CompositeContainerCustomizer(List> customizers) { @Override public void configure(C container) { - customizers.forEach(c -> c.configure(container)); + this.customizers.forEach(c -> c.configure(container)); } } From 77dfa657ddb437c8ca97eadfd9f1808e12083fb4 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 20 Oct 2022 15:59:40 -0400 Subject: [PATCH 180/737] GH-1477: Reduce Log Noise While Broker Down Resolves https://github.com/spring-projects/spring-amqp/issues/1477 Only log the stack trace on the first failure. Tested with a Boot app and stop/start/stop the broker. **cherry-pick to 2.4.x** --- .../listener/AbstractMessageListenerContainer.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index 28e621136a..156c55c0cb 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -28,6 +28,7 @@ import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import org.aopalliance.aop.Advice; @@ -147,6 +148,8 @@ public abstract class AbstractMessageListenerContainer extends RabbitAccessor private ContainerDelegate proxy = this.delegate; + private final AtomicBoolean logDeclarationException = new AtomicBoolean(true); + private long shutdownTimeout = DEFAULT_SHUTDOWN_TIMEOUT; private ApplicationEventPublisher applicationEventPublisher; @@ -1960,12 +1963,18 @@ protected synchronized void redeclareElementsIfNecessary() { if (!this.lazyLoad && admin != null && isAutoDeclare()) { try { attemptDeclarations(admin); + this.logDeclarationException.set(true); } catch (Exception e) { if (RabbitUtils.isMismatchedQueueArgs(e)) { throw new FatalListenerStartupException("Mismatched queues", e); } - logger.error("Failed to check/redeclare auto-delete queue(s).", e); + if (this.logDeclarationException.getAndSet(false)) { + this.logger.error("Failed to check/redeclare auto-delete queue(s).", e); + } + else { + this.logger.error("Failed to check/redeclare auto-delete queue(s)."); + } } } } From 36e98a735fcb19bf102009c245756bbadfbd10a4 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Fri, 21 Oct 2022 16:54:40 -0400 Subject: [PATCH 181/737] GH-1444: Observation Doc Gen Polishing - single task for spans and metrics - always gen and remove packages from file names * Fix link in What's New; add observation properties to container factory. --- build.gradle | 54 +++++++++---------- ...bstractRabbitListenerContainerFactory.java | 43 ++++++++++++++- src/reference/asciidoc/_conventions.adoc | 11 ---- src/reference/asciidoc/_metrics.adoc | 44 --------------- src/reference/asciidoc/_spans.adoc | 38 ------------- src/reference/asciidoc/appendix.adoc | 8 +-- src/reference/asciidoc/whats-new.adoc | 2 +- 7 files changed, 71 insertions(+), 129 deletions(-) delete mode 100644 src/reference/asciidoc/_conventions.adoc delete mode 100644 src/reference/asciidoc/_metrics.adoc delete mode 100644 src/reference/asciidoc/_spans.adoc diff --git a/build.gradle b/build.gradle index 4f2f21bcce..1dce809896 100644 --- a/build.gradle +++ b/build.gradle @@ -56,7 +56,7 @@ ext { log4jVersion = '2.19.0' logbackVersion = '1.4.4' lz4Version = '1.8.0' - micrometerDocsVersion = '1.0.0-RC1' + micrometerDocsVersion = '1.0.0-SNAPSHOT' micrometerVersion = '1.10.0-RC1' micrometerTracingVersion = '1.0.0-RC1' mockitoVersion = '4.8.0' @@ -419,36 +419,8 @@ project('spring-rabbit') { testRuntimeOnly ("junit:junit:$junit4Version") { exclude group: 'org.hamcrest', module: 'hamcrest-core' - adoc "io.micrometer:micrometer-docs-generator-spans:$micrometerDocsVersion" - adoc "io.micrometer:micrometer-docs-generator-metrics:$micrometerDocsVersion" - - } - - def inputDir = file('src/main/java/org/springframework/amqp/rabbit/support/micrometer').absolutePath - def outputDir = rootProject.file('src/reference/asciidoc').absolutePath - - task generateObservabilityMetricsDocs(type: JavaExec) { - onlyIf { !isCI } - mainClass = 'io.micrometer.docs.metrics.DocsFromSources' - inputs.dir(inputDir) - outputs.dir(outputDir) - classpath configurations.adoc - args inputDir, '.*', outputDir - } - - task generateObservabilitySpansDocs(type: JavaExec) { - onlyIf { !isCI } - mainClass = 'io.micrometer.docs.spans.DocsFromSources' - inputs.dir(inputDir) - outputs.dir(outputDir) - classpath configurations.adoc - args inputDir, '.*', outputDir } - // javadoc { - // finalizedBy generateObservabilityMetricsDocs, generateObservabilitySpansDocs - // } - } compileTestKotlin { @@ -522,10 +494,12 @@ project('spring-rabbit-test') { configurations { asciidoctorExtensions + micrometerDocs } dependencies { asciidoctorExtensions "io.spring.asciidoctor.backends:spring-asciidoctor-backends:${springAsciidoctorBackendsVersion}" + micrometerDocs "io.micrometer:micrometer-docs-generator:$micrometerDocsVersion" } task prepareAsciidocBuild(type: Sync) { @@ -535,8 +509,28 @@ task prepareAsciidocBuild(type: Sync) { into "$buildDir/asciidoc" } +def observationInputDir = file('spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer').absolutePath +def generatedDocsDir = file("$buildDir/docs/generated").absolutePath + +task generateObservabilityDocs(type: JavaExec) { + mainClass = 'io.micrometer.docs.DocsGeneratorCommand' + inputs.dir(observationInputDir) + outputs.dir(generatedDocsDir) + classpath configurations.micrometerDocs + args observationInputDir, /.+/, generatedDocsDir +} + +task filterMetricsDocsContent(type: Copy) { + dependsOn generateObservabilityDocs + from generatedDocsDir + include '_*.adoc' + into generatedDocsDir + rename { filename -> filename.replace '_', '' } + filter { line -> line.replaceAll('org.springframework.amqp.rabbit.support.micrometer.', '').replaceAll('^Fully qualified n', 'N') } +} + asciidoctorPdf { - dependsOn prepareAsciidocBuild + dependsOn prepareAsciidocBuild, filterMetricsDocsContent baseDirFollowsSourceFile() asciidoctorj { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java index 086d8ff1dc..2f38812a35 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java @@ -32,6 +32,7 @@ import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; import org.springframework.amqp.rabbit.listener.MessageAckListener; import org.springframework.amqp.rabbit.listener.RabbitListenerEndpoint; +import org.springframework.amqp.rabbit.support.micrometer.RabbitListenerObservationConvention; import org.springframework.amqp.support.ConsumerTagStrategy; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.amqp.utils.JavaUtils; @@ -118,6 +119,12 @@ public abstract class AbstractRabbitListenerContainerFactory Observation for Rabbit listeners. - -**Span name** `spring.rabbit.listener` (defined by convention class `RabbitListenerObservation$DefaultRabbitListenerObservationConvention`). - -Name of the enclosing class `RabbitListenerObservation`. - -IMPORTANT: All tags and event names must be prefixed with `spring.rabbit.listener` prefix! - -.Tag Keys -|=== -|Name | Description -|`spring.rabbit.listener.id`|Listener id. -|=== - -[[observability-spans-template-observation]] -==== Template Observation Span - -> Observation for `RabbitTemplate` s. - -**Span name** `spring.rabbit.template` (defined by convention class `RabbitTemplateObservation$DefaultRabbitTemplateObservationConvention`). - -Name of the enclosing class `RabbitTemplateObservation`. - -IMPORTANT: All tags and event names must be prefixed with `spring.rabbit.template` prefix! - -.Tag Keys -|=== -|Name | Description -|`spring.rabbit.template.name`|Bean name of the template. -|=== diff --git a/src/reference/asciidoc/appendix.adoc b/src/reference/asciidoc/appendix.adoc index 294ffb4c5e..e18b92e413 100644 --- a/src/reference/asciidoc/appendix.adoc +++ b/src/reference/asciidoc/appendix.adoc @@ -2,17 +2,17 @@ [[observation-gen]] == Micrometer Observation Documentation -include::_metrics.adoc[] +include::../docs/generated/metrics.adoc[] -include::_spans.adoc[] +include::../docs/generated/spans.adoc[] -include::_conventions.adoc[] +include::../docs/generated/conventions.adoc[] [appendix] [[change-history]] == Change History -This section describes what changes have been made as versions have changed. +This section describes changes that have been made as versions have changed. === Current Release diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index ebcbe08a35..912926c37b 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -14,7 +14,7 @@ The remoting feature (using RMI) is no longer supported. ==== Observation Enabling observation for timers and tracing using Micrometer is now supported. -See <> for more information. +See <> for more information. ==== AsyncRabbitTemplate From 0459c9907cf2de4e5045815f0e0e819ec5d75014 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 26 Oct 2022 12:09:33 -0400 Subject: [PATCH 182/737] GH-1524: Fix ThreadChannelCF with Transactional Resolves https://github.com/spring-projects/spring-amqp/issues/1524 Channel was always physically closed. **cherry-pick to 2.4.x** --- .../ThreadChannelConnectionFactory.java | 3 +- .../ThreadChannelConnectionFactoryTests.java | 32 ++++++++++++++++++- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java index 6433b324d1..652164f462 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java @@ -307,8 +307,7 @@ private Channel createProxy(Channel channel, boolean transactional) { } private void handleClose(Channel channel, boolean transactional) { - - if (transactional && this.txChannels.get() == null ? true : this.channels.get() == null) { + if ((transactional && this.txChannels.get() == null) || (!transactional && this.channels.get() == null)) { physicalClose(channel); } else { diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactoryTests.java index fe3ec528ef..5b34ec4649 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-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. @@ -110,6 +110,36 @@ void testBasic() throws Exception { .isFalse(); } + @Test + void testClose() throws Exception { + ConnectionFactory rabbitConnectionFactory = new ConnectionFactory(); + rabbitConnectionFactory.setHost("localhost"); + ThreadChannelConnectionFactory tccf = new ThreadChannelConnectionFactory(rabbitConnectionFactory); + Connection conn = tccf.createConnection(); + Channel chann1 = conn.createChannel(false); + Channel targetChannel1 = ((ChannelProxy) chann1).getTargetChannel(); + chann1.close(); + Channel chann2 = conn.createChannel(false); + Channel targetChannel2 = ((ChannelProxy) chann2).getTargetChannel(); + assertThat(chann2).isSameAs(chann1); + assertThat(targetChannel2).isSameAs(targetChannel1); + } + + @Test + void testTxClose() throws Exception { + ConnectionFactory rabbitConnectionFactory = new ConnectionFactory(); + rabbitConnectionFactory.setHost("localhost"); + ThreadChannelConnectionFactory tccf = new ThreadChannelConnectionFactory(rabbitConnectionFactory); + Connection conn = tccf.createConnection(); + Channel chann1 = conn.createChannel(true); + Channel targetChannel1 = ((ChannelProxy) chann1).getTargetChannel(); + chann1.close(); + Channel chann2 = conn.createChannel(true); + Channel targetChannel2 = ((ChannelProxy) chann2).getTargetChannel(); + assertThat(chann2).isSameAs(chann1); + assertThat(targetChannel2).isSameAs(targetChannel1); + } + @Test void queueDeclared(@Autowired RabbitAdmin admin, @Autowired Config config, @Autowired ThreadChannelConnectionFactory tccf) throws Exception { From f2fc13b6e1104a54bd141e3ed8f8cf32f98e3df5 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 26 Oct 2022 14:20:28 -0400 Subject: [PATCH 183/737] GH-1425: Configure ReplyPostProcessor via Factory Resolves https://github.com/spring-projects/spring-amqp/issues/1425 --- .../BaseRabbitListenerContainerFactory.java | 20 +++++++++++++++++++ .../EnableRabbitIntegrationTests.java | 15 +++++++++++++- src/reference/asciidoc/amqp.adoc | 16 +++++++++++++++ src/reference/asciidoc/whats-new.adoc | 5 ++++- 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BaseRabbitListenerContainerFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BaseRabbitListenerContainerFactory.java index 339c7e08b2..51e0102027 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BaseRabbitListenerContainerFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BaseRabbitListenerContainerFactory.java @@ -17,6 +17,7 @@ package org.springframework.amqp.rabbit.config; import java.util.Arrays; +import java.util.function.Function; import org.aopalliance.aop.Advice; @@ -26,6 +27,7 @@ import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory; import org.springframework.amqp.rabbit.listener.RabbitListenerEndpoint; import org.springframework.amqp.rabbit.listener.adapter.AbstractAdaptableMessageListener; +import org.springframework.amqp.rabbit.listener.adapter.ReplyPostProcessor; import org.springframework.amqp.utils.JavaUtils; import org.springframework.lang.Nullable; import org.springframework.retry.RecoveryCallback; @@ -54,6 +56,8 @@ public abstract class BaseRabbitListenerContainerFactory replyPostProcessorProvider; + @Override public abstract C createListenerContainer(RabbitListenerEndpoint endpoint); @@ -108,6 +112,17 @@ public void setReplyRecoveryCallback(RecoveryCallback recoveryCallback) { this.recoveryCallback = recoveryCallback; } + /** + * Set a function to provide a reply post processor; it will be used if there is no + * replyPostProcessor on the rabbit listener annotation. The input parameter is the + * listener id. + * @param replyPostProcessorProvider the post processor. + * @since 3.0 + */ + public void setReplyPostProcessorProvider(Function replyPostProcessorProvider) { + this.replyPostProcessorProvider = replyPostProcessorProvider; + } + protected void applyCommonOverrides(@Nullable RabbitListenerEndpoint endpoint, C instance) { if (endpoint != null) { // endpoint settings overriding default factory settings JavaUtils.INSTANCE @@ -130,6 +145,11 @@ protected void applyCommonOverrides(@Nullable RabbitListenerEndpoint endpoint, C .acceptIfNotNull(endpoint.getReplyPostProcessor(), messageListener::setReplyPostProcessor) .acceptIfNotNull(endpoint.getReplyContentType(), messageListener::setReplyContentType); messageListener.setConverterWinsContentType(endpoint.isConverterWinsContentType()); + if (endpoint.getReplyPostProcessor() == null && this.replyPostProcessorProvider != null) { + JavaUtils.INSTANCE + .acceptIfNotNull(this.replyPostProcessorProvider.apply(endpoint.getId()), + messageListener::setReplyPostProcessor); + } } } } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java index 6eec2a3162..af286a88cd 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java @@ -237,6 +237,9 @@ public class EnableRabbitIntegrationTests extends NeedsManagementTests { @Autowired private MultiListenerValidatedJsonBean multiValidated; + @Autowired + private ReplyPostProcessor rpp; + @BeforeAll public static void setUp() { System.setProperty(RabbitListenerAnnotationBeanPostProcessor.RABBIT_EMPTY_STRING_ARGUMENTS_PROPERTY, @@ -310,6 +313,8 @@ public void autoStart() { this.registry.start(); assertThat(listenerContainer.isRunning()).isTrue(); listenerContainer.stop(); + assertThat(listenerContainer.getMessageListener()).extracting("replyPostProcessor") + .isSameAs(this.rpp); } @Test @@ -1690,14 +1695,22 @@ public SimpleMessageListenerContainer factoryCreatedContainerNoListener( } @Bean - public SimpleRabbitListenerContainerFactory rabbitAutoStartFalseListenerContainerFactory() { + public SimpleRabbitListenerContainerFactory rabbitAutoStartFalseListenerContainerFactory( + ReplyPostProcessor rpp) { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); factory.setConnectionFactory(rabbitConnectionFactory()); factory.setReceiveTimeout(10L); factory.setAutoStartup(false); + factory.setReplyPostProcessorProvider(id -> rpp); return factory; } + @Bean + ReplyPostProcessor rpp() { + return (in, out) -> out; + } + @Bean public SimpleRabbitListenerContainerFactory jsonListenerContainerFactory() { SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index 67a3d8511e..ff6edd75b6 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -3144,6 +3144,22 @@ public ReplyPostProcessor echoCustomHeader() { ---- ==== +Starting with version 3.0, you can configure the post processor on the container factory instead of on the annotation. + +==== +[source, java] +---- +factory.setReplyPostProcessorProvider(id -> (req, resp) -> { + resp.getMessageProperties().setHeader("myHeader", req.getMessageProperties().getHeader("myHeader")); + return resp; +}); +---- +==== + +The `id` parameter is the listener id. + +A setting on the annotation will supersede the factory setting. + 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. The valid values are as follows: diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index 912926c37b..a55a2d40c5 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -37,7 +37,10 @@ When setting the container factory `consumerBatchEnabled` to `true`, the `batchL See <> for more infoprmation. `MessageConverter` s can now return `Optional.empty()` for a null value; this is currently implemented by the `Jackson2JsonMessageConverter`. -See <> for more information. +See <> for more information + +You can now configure a `ReplyPostProcessor` via the container factory rather than via a property on `@RabbitListener`. +See <> for more information. ==== Connection Factory Changes From 41802039a5af66f56432155897df72842708d1a0 Mon Sep 17 00:00:00 2001 From: Christoph Dreis Date: Fri, 28 Oct 2022 18:50:50 +0200 Subject: [PATCH 184/737] GH-1528: Fix Possible Type Pollution GH-1528: Fix possible type pollution in RabbitListenerAnnotationBeanPostProcessor Resolves #1528 --- .../RabbitListenerAnnotationBeanPostProcessor.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java index 0f41248539..1c7da2ae6c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java @@ -323,12 +323,12 @@ public Object postProcessAfterInitialization(final Object bean, final String bea } private TypeMetadata buildMetadata(Class targetClass) { - Collection classLevelListeners = findListenerAnnotations(targetClass); + List classLevelListeners = findListenerAnnotations(targetClass); final boolean hasClassLevelListeners = classLevelListeners.size() > 0; final List methods = new ArrayList<>(); final List multiMethods = new ArrayList<>(); ReflectionUtils.doWithMethods(targetClass, method -> { - Collection listenerAnnotations = findListenerAnnotations(method); + List listenerAnnotations = findListenerAnnotations(method); if (listenerAnnotations.size() > 0) { methods.add(new ListenerMethod(method, listenerAnnotations.toArray(new RabbitListener[listenerAnnotations.size()]))); @@ -349,7 +349,7 @@ private TypeMetadata buildMetadata(Class targetClass) { classLevelListeners.toArray(new RabbitListener[classLevelListeners.size()])); } - private Collection findListenerAnnotations(AnnotatedElement element) { + private List findListenerAnnotations(AnnotatedElement element) { return MergedAnnotations.from(element, SearchStrategy.TYPE_HIERARCHY) .stream(RabbitListener.class) .map(ann -> ann.synthesize()) From 79808a84b39adb5bf38f919dbfeb1c8b787c7aae Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 27 Oct 2022 15:30:26 -0400 Subject: [PATCH 185/737] GH-1382: Republish Recoverer Improvements Resolves https://github.com/spring-projects/spring-amqp/issues/1382 Add expressions; make private method protected. **cherry-pick to 2.4.x** --- .../retry/RepublishMessageRecoverer.java | 76 +++++++++++++++---- ...va => RepublishMessageRecovererTests.java} | 30 +++++++- src/reference/asciidoc/amqp.adoc | 5 +- 3 files changed, 94 insertions(+), 17 deletions(-) rename spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/{RepublishMessageRecovererTest.java => RepublishMessageRecovererTests.java} (81%) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecoverer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecoverer.java index 271b5c2d75..efe28d49f5 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecoverer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecoverer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 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. @@ -29,6 +29,10 @@ import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.rabbit.connection.RabbitUtils; import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.common.LiteralExpression; +import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -68,9 +72,11 @@ public class RepublishMessageRecoverer implements MessageRecoverer { protected final AmqpTemplate errorTemplate; // NOSONAR - protected final String errorRoutingKey; // NOSONAR + protected final Expression errorRoutingKeyExpression; // NOSONAR - protected final String errorExchangeName; // NOSONAR + protected final Expression errorExchangeNameExpression; // NOSONAR + + protected final EvaluationContext evaluationContext = new StandardEvaluationContext(); private String errorRoutingKeyPrefix = "error."; @@ -80,19 +86,48 @@ public class RepublishMessageRecoverer implements MessageRecoverer { private MessageDeliveryMode deliveryMode = MessageDeliveryMode.PERSISTENT; + /** + * Create an instance with the provided template. + * @param errorTemplate the template. + */ public RepublishMessageRecoverer(AmqpTemplate errorTemplate) { - this(errorTemplate, null, null); + this(errorTemplate, (String) null, (String) null); } + /** + * Create an instance with the provided properties. + * @param errorTemplate the template. + * @param errorExchange the exchange. + */ public RepublishMessageRecoverer(AmqpTemplate errorTemplate, String errorExchange) { this(errorTemplate, errorExchange, null); } + /** + * Create an instance with the provided properties. If the exchange or routing key is null, + * the template's default will be used. + * @param errorTemplate the template. + * @param errorExchange the exchange. + * @param errorRoutingKey the routing key. + */ public RepublishMessageRecoverer(AmqpTemplate errorTemplate, String errorExchange, String errorRoutingKey) { + this(errorTemplate, new LiteralExpression(errorExchange), new LiteralExpression(errorRoutingKey)); + } + + /** + * Create an instance with the provided properties. If the exchange or routing key + * evaluate to null, the template's default will be used. + * @param errorTemplate the template. + * @param errorExchange the exchange expression, evaluated against the message. + * @param errorRoutingKey the routing key, evaluated against the message. + */ + public RepublishMessageRecoverer(AmqpTemplate errorTemplate, @Nullable Expression errorExchange, + @Nullable Expression errorRoutingKey) { + Assert.notNull(errorTemplate, "'errorTemplate' cannot be null"); this.errorTemplate = errorTemplate; - this.errorExchangeName = errorExchange; - this.errorRoutingKey = errorRoutingKey; + this.errorExchangeNameExpression = errorExchange != null ? errorExchange : new LiteralExpression(null); + this.errorRoutingKeyExpression = errorRoutingKey != null ? errorRoutingKey : new LiteralExpression(null); if (!(this.errorTemplate instanceof RabbitTemplate)) { this.maxStackTraceLength = Integer.MAX_VALUE; } @@ -175,17 +210,17 @@ public void recover(Message message, Throwable cause) { messageProperties.setDeliveryMode(this.deliveryMode); } - if (null != this.errorExchangeName) { - String routingKey = this.errorRoutingKey != null ? this.errorRoutingKey - : this.prefixedOriginalRoutingKey(message); - doSend(this.errorExchangeName, routingKey, message); + String exchangeName = this.errorExchangeNameExpression.getValue(this.evaluationContext, message, String.class); + String rk = this.errorRoutingKeyExpression.getValue(this.evaluationContext, message, String.class); + String routingKey = rk != null ? rk : this.prefixedOriginalRoutingKey(message); + if (null != exchangeName) { + doSend(exchangeName, routingKey, message); if (this.logger.isWarnEnabled()) { - this.logger.warn("Republishing failed message to exchange '" + this.errorExchangeName + this.logger.warn("Republishing failed message to exchange '" + exchangeName + "' with routing key " + routingKey); } } else { - final String routingKey = this.prefixedOriginalRoutingKey(message); doSend(null, routingKey, message); if (this.logger.isWarnEnabled()) { this.logger.warn("Republishing failed message to the template's default exchange with routing key " @@ -271,11 +306,24 @@ else if (stackTraceAsString.length() + exceptionMessage.length() > this.maxStack return null; } - private String prefixedOriginalRoutingKey(Message message) { + /** + * The default behavior of this method is to append the received routing key to the + * {@link #setErrorRoutingKeyPrefix(String) routingKeyPrefix}. This is only invoked + * if the routing key is null. + * @param message the message. + * @return the routing key. + */ + protected String prefixedOriginalRoutingKey(Message message) { return this.errorRoutingKeyPrefix + message.getMessageProperties().getReceivedRoutingKey(); } - private String getStackTraceAsString(Throwable cause) { + /** + * Create a String representation of the stack trace. + * @param cause the throwable. + * @return the String. + * @since 2.4.8 + */ + protected String getStackTraceAsString(Throwable cause) { StringWriter stringWriter = new StringWriter(); PrintWriter printWriter = new PrintWriter(stringWriter, true); cause.printStackTrace(printWriter); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererTest.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererTests.java similarity index 81% rename from spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererTest.java rename to spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererTests.java index 4f5e912a26..675c697426 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererTest.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 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. @@ -33,6 +33,7 @@ import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageDeliveryMode; import org.springframework.amqp.core.MessageProperties; +import org.springframework.expression.spel.standard.SpelExpressionParser; /** * @author James Carr @@ -42,7 +43,7 @@ * @since 1.3 */ @ExtendWith(MockitoExtension.class) -public class RepublishMessageRecovererTest { +public class RepublishMessageRecovererTests { private final Message message = new Message("".getBytes(), new MessageProperties()); @@ -151,4 +152,29 @@ void setDeliveryModeIfNull() { assertThat(this.message.getMessageProperties().getDeliveryMode()).isEqualTo(MessageDeliveryMode.NON_PERSISTENT); } + @Test + void dynamicExRk() { + this.recoverer = new RepublishMessageRecoverer(this.amqpTemplate, + new SpelExpressionParser().parseExpression("messageProperties.headers.get('errorExchange')"), + new SpelExpressionParser().parseExpression("messageProperties.headers.get('errorRK')")); + this.message.getMessageProperties().setHeader("errorExchange", "ex"); + this.message.getMessageProperties().setHeader("errorRK", "rk"); + + this.recoverer.recover(this.message, this.cause); + + verify(this.amqpTemplate).send("ex", "rk", this.message); + } + + @Test + void dynamicRk() { + this.recoverer = new RepublishMessageRecoverer(this.amqpTemplate, null, + new SpelExpressionParser().parseExpression("messageProperties.headers.get('errorRK')")); + this.message.getMessageProperties().setHeader("errorExchange", "ex"); + this.message.getMessageProperties().setHeader("errorRK", "rk"); + + this.recoverer.recover(this.message, this.cause); + + verify(this.amqpTemplate).send("rk", this.message); + } + } diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index ff6edd75b6..c015048056 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -6742,7 +6742,7 @@ The default `MessageRecoverer` consumes the errant message and emits a `WARN` me Starting with version 1.3, a new `RepublishMessageRecoverer` is provided, to allow publishing of failed messages after retries are exhausted. -When a recoverer consumes the final exception, the message is ack'd and is not sent to the dead letter exchange, if any. +When a recoverer consumes the final exception, the message is ack'd and is not sent to the dead letter exchange by the broker, if configured. NOTE: When `RepublishMessageRecoverer` is used on the consumer side, the received message has `deliveryMode` in the `receivedDeliveryMode` message property. In this case the `deliveryMode` is `null`. @@ -6793,6 +6793,9 @@ Starting with versions 2.1.13, 2.2.3, the exception message is included in this * if the stack trace is small, the message will be truncated (plus `...`) to fit in the available bytes (but the message within the stack trace itself is truncated to 97 bytes plus `...`). Whenever a truncation of any kind occurs, the original exception will be logged to retain the complete information. +The evaluation is performed after the headers are enhanced so information such as the exception type can be used in the expressions. + +Starting with version 2.4.8, the error exchange and routing key can be provided as SpEL expressions, with the `Message` being the root object for the evaluation. Starting with version 2.3.3, a new subclass `RepublishMessageRecovererWithConfirms` is provided; this supports both styles of publisher confirms and will wait for the confirmation before returning (or throw an exception if not confirmed or the message is returned). From 8a478620257774eda06c1d7dbc918f4a53c5aa3d Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 31 Oct 2022 08:47:42 -0400 Subject: [PATCH 186/737] GH-1382: Sonar Issues --- .../amqp/rabbit/retry/RepublishMessageRecoverer.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecoverer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecoverer.java index efe28d49f5..304e4f7a97 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecoverer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecoverer.java @@ -110,7 +110,9 @@ public RepublishMessageRecoverer(AmqpTemplate errorTemplate, String errorExchang * @param errorExchange the exchange. * @param errorRoutingKey the routing key. */ - public RepublishMessageRecoverer(AmqpTemplate errorTemplate, String errorExchange, String errorRoutingKey) { + public RepublishMessageRecoverer(AmqpTemplate errorTemplate, @Nullable String errorExchange, + @Nullable String errorRoutingKey) { + this(errorTemplate, new LiteralExpression(errorExchange), new LiteralExpression(errorRoutingKey)); } @@ -126,8 +128,8 @@ public RepublishMessageRecoverer(AmqpTemplate errorTemplate, @Nullable Expressio Assert.notNull(errorTemplate, "'errorTemplate' cannot be null"); this.errorTemplate = errorTemplate; - this.errorExchangeNameExpression = errorExchange != null ? errorExchange : new LiteralExpression(null); - this.errorRoutingKeyExpression = errorRoutingKey != null ? errorRoutingKey : new LiteralExpression(null); + this.errorExchangeNameExpression = errorExchange != null ? errorExchange : new LiteralExpression(null); // NOSONAR + this.errorRoutingKeyExpression = errorRoutingKey != null ? errorRoutingKey : new LiteralExpression(null); // NOSONAR if (!(this.errorTemplate instanceof RabbitTemplate)) { this.maxStackTraceLength = Integer.MAX_VALUE; } From b9b205feff92d177f326b003d3c29560d35993ee Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 1 Nov 2022 13:49:48 -0400 Subject: [PATCH 187/737] TestContainers Compatibility, Version --- build.gradle | 2 +- .../amqp/rabbit/junit/AbstractTestContainerTests.java | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 1dce809896..5e0f6687d6 100644 --- a/build.gradle +++ b/build.gradle @@ -67,7 +67,7 @@ ext { springDataVersion = '2022.0.0-RC1' springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.0-RC1' springRetryVersion = '2.0.0-RC1' - testContainersVersion = '1.17.3' + testContainersVersion = '1.17.5' zstdJniVersion = '1.5.0-2' } diff --git a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/AbstractTestContainerTests.java b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/AbstractTestContainerTests.java index 7504a46595..12c49e6399 100644 --- a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/AbstractTestContainerTests.java +++ b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/AbstractTestContainerTests.java @@ -19,6 +19,7 @@ import java.time.Duration; import org.testcontainers.containers.RabbitMQContainer; +import org.testcontainers.utility.DockerImageName; /** * @author Gary Russell @@ -37,7 +38,9 @@ public abstract class AbstractTestContainerTests { if (cache != null) { image = cache + image; } - RABBITMQ = new RabbitMQContainer(image) + DockerImageName imageName = DockerImageName.parse(image) + .asCompatibleSubstituteFor("rabbitmq"); + RABBITMQ = new RabbitMQContainer(imageName) .withExposedPorts(5672, 15672, 5552) .withPluginsEnabled("rabbitmq_stream") .withStartupTimeout(Duration.ofMinutes(2)); From e7d7244b309d1b38c5d9b16f5a57cb7b77e61373 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 1 Nov 2022 14:41:07 -0400 Subject: [PATCH 188/737] Address Sonar Issues --- .../rabbit/stream/listener/StreamListenerContainer.java | 2 +- .../rabbit/stream/producer/RabbitStreamTemplate.java | 2 +- .../RabbitListenerAnnotationBeanPostProcessor.java | 8 ++++---- .../amqp/rabbit/config/BindingFactoryBean.java | 2 +- .../amqp/rabbit/config/ListenerContainerFactoryBean.java | 8 ++++---- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java index 33926f5e3c..201abed302 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java @@ -130,7 +130,7 @@ public void superStream(String streamName, String name) { * @param consumers the number of consumers. * @since 3.0 */ - public void superStream(String streamName, String name, int consumers) { + public synchronized void superStream(String streamName, String name, int consumers) { Assert.isTrue(consumers > 0, () -> "'concurrency' must be greater than zero, not " + consumers); this.concurrency = consumers; Assert.isTrue(!this.simpleStream, "setQueueNames() and superStream() are mutually exclusive"); diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java index 418c22bf37..2c7c71bf60 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java @@ -110,7 +110,7 @@ public synchronized void setBeanName(String name) { * @param superStreamRouting the routing function. * @since 3.0 */ - public void setSuperStreamRouting(Function superStreamRouting) { + public synchronized void setSuperStreamRouting(Function superStreamRouting) { this.superStreamRouting = superStreamRouting; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java index 1c7da2ae6c..02a76135a7 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java @@ -1126,9 +1126,9 @@ private TypeMetadata() { } TypeMetadata(ListenerMethod[] methods, Method[] multiMethods, RabbitListener[] classLevelListeners) { // NOSONAR - this.listenerMethods = methods; - this.handlerMethods = multiMethods; - this.classAnnotations = classLevelListeners; + this.listenerMethods = methods; // NOSONAR + this.handlerMethods = multiMethods; // NOSONAR + this.classAnnotations = classLevelListeners; // NOSONAR } } @@ -1144,7 +1144,7 @@ private static class ListenerMethod { ListenerMethod(Method method, RabbitListener[] annotations) { // NOSONAR this.method = method; - this.annotations = annotations; + this.annotations = annotations; // NOSONAR } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BindingFactoryBean.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BindingFactoryBean.java index af8ca15a29..1796d0adc4 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BindingFactoryBean.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BindingFactoryBean.java @@ -77,7 +77,7 @@ public void setIgnoreDeclarationExceptions(Boolean ignoreDeclarationExceptions) } public void setAdminsThatShouldDeclare(AmqpAdmin... admins) { // NOSONAR - this.adminsThatShouldDeclare = admins; + this.adminsThatShouldDeclare = admins; // NOSONAR } @Override diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java index bbe884e25a..a23d29ca83 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java @@ -212,11 +212,11 @@ public void setAcknowledgeMode(AcknowledgeMode acknowledgeMode) { } public void setQueueNames(String... queueName) { // NOSONAR - this.queueNames = queueName; + this.queueNames = queueName; // NOSONAR } public void setQueues(Queue... queues) { // NOSONAR - this.queues = queues; + this.queues = queues; // NOSONAR } public void setExposeListenerChannel(boolean exposeListenerChannel) { @@ -236,11 +236,11 @@ public void setDeBatchingEnabled(boolean deBatchingEnabled) { } public void setAdviceChain(Advice... adviceChain) { // NOSONAR - this.adviceChain = adviceChain; + this.adviceChain = adviceChain; // NOSONAR } public void setAfterReceivePostProcessors(MessagePostProcessor... afterReceivePostProcessors) { // NOSONAR - this.afterReceivePostProcessors = afterReceivePostProcessors; + this.afterReceivePostProcessors = afterReceivePostProcessors; // NOSONAR } public void setAutoStartup(boolean autoStartup) { From cdcaa6e9a62be2ea7680e60a2c45cf7a943784e8 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 2 Nov 2022 11:37:48 -0400 Subject: [PATCH 189/737] Address Sonar Issues --- ...teInvocationAwareMessageConverterAdapter.java | 6 +----- .../stream/listener/StreamListenerContainer.java | 2 +- .../connection/ConsumerChannelRegistry.java | 4 ++-- .../amqp/rabbit/connection/RabbitAccessor.java | 1 - .../amqp/rabbit/core/RabbitAdmin.java | 16 +++++++--------- .../AbstractMessageListenerContainer.java | 2 +- .../adapter/MessagingMessageListenerAdapter.java | 7 +++---- .../rabbit/retry/RepublishMessageRecoverer.java | 2 +- .../micrometer/RabbitMessageReceiverContext.java | 1 + 9 files changed, 17 insertions(+), 24 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationAwareMessageConverterAdapter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationAwareMessageConverterAdapter.java index 41199af6bc..61fe092e68 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationAwareMessageConverterAdapter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationAwareMessageConverterAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 the original author or authors. + * Copyright 2016-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. @@ -34,17 +34,13 @@ public class RemoteInvocationAwareMessageConverterAdapter implements MessageConv private final MessageConverter delegate; - private final boolean shouldSetClassLoader; - public RemoteInvocationAwareMessageConverterAdapter() { this.delegate = new SimpleMessageConverter(); - this.shouldSetClassLoader = true; } public RemoteInvocationAwareMessageConverterAdapter(MessageConverter delegate) { Assert.notNull(delegate, "'delegate' converter cannot be null"); this.delegate = delegate; - this.shouldSetClassLoader = false; } @Override diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java index 201abed302..ea5d2fe8d1 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java @@ -103,7 +103,7 @@ public StreamListenerContainer(Environment environment, @Nullable Codec codec) { * Mutually exclusive with {@link #superStream(String, String)}. */ @Override - public void setQueueNames(String... queueNames) { + public synchronized void setQueueNames(String... queueNames) { Assert.isTrue(!this.superStream, "setQueueNames() and superStream() are mutually exclusive"); Assert.isTrue(queueNames != null && queueNames.length == 1, "Only one stream is supported"); this.builder.stream(queueNames[0]); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConsumerChannelRegistry.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConsumerChannelRegistry.java index 1d2b61abef..0aac6947b3 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConsumerChannelRegistry.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConsumerChannelRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-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. @@ -101,7 +101,7 @@ public static Channel getConsumerChannel() { public static Channel getConsumerChannel(ConnectionFactory connectionFactory) { ChannelHolder channelHolder = consumerChannel.get(); Channel channel = null; - if (channelHolder != null && channelHolder.getConnectionFactory() == connectionFactory) { + if (channelHolder != null && channelHolder.getConnectionFactory().equals(connectionFactory)) { channel = channelHolder.getChannel(); } return channel; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java index b8c23f630f..88d7d9b65b 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java @@ -126,7 +126,6 @@ protected void obtainObservationRegistry(@Nullable ApplicationContext appContext } } - @Nullable protected ObservationRegistry getObservationRegistry() { return this.observationRegistry; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java index 6644c447e4..4cfb540d9e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java @@ -264,11 +264,10 @@ private void removeExchangeBindings(final String exchangeName) { Iterator> iterator = this.manualDeclarables.entrySet().iterator(); while (iterator.hasNext()) { Entry next = iterator.next(); - if (next.getValue() instanceof Binding binding) { - if ((!binding.isDestinationQueue() && binding.getDestination().equals(exchangeName)) - || binding.getExchange().equals(exchangeName)) { - iterator.remove(); - } + if (next.getValue() instanceof Binding binding && + ((!binding.isDestinationQueue() && binding.getDestination().equals(exchangeName)) + || binding.getExchange().equals(exchangeName))) { + iterator.remove(); } } } @@ -362,10 +361,9 @@ private void removeQueueBindings(final String queueName) { Iterator> iterator = this.manualDeclarables.entrySet().iterator(); while (iterator.hasNext()) { Entry next = iterator.next(); - if (next.getValue() instanceof Binding binding) { - if (binding.isDestinationQueue() && binding.getDestination().equals(queueName)) { - iterator.remove(); - } + if (next.getValue() instanceof Binding binding && + (binding.isDestinationQueue() && binding.getDestination().equals(queueName))) { + iterator.remove(); } } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index 156c55c0cb..3a76dab646 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -1642,7 +1642,7 @@ protected void actualInvokeListener(Channel channel, Object data) { if (listener instanceof ChannelAwareMessageListener chaml) { doInvokeListener(chaml, channel, data); } - else if (listener instanceof MessageListener msgListener) { + else if (listener instanceof MessageListener msgListener) { // NOSONAR boolean bindChannel = isExposeListenerChannel() && isChannelLocallyTransacted(); if (bindChannel) { RabbitResourceHolder resourceHolder = new RabbitResourceHolder(channel, false); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java index 27fa54ead0..d38bab75cf 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java @@ -436,10 +436,9 @@ private boolean isEligibleParameter(MethodParameter methodParameter) { || parameterType.equals(org.springframework.amqp.core.Message.class)) { return false; } - if (parameterType instanceof ParameterizedType parameterizedType) { - if (parameterizedType.getRawType().equals(Message.class)) { - return !(parameterizedType.getActualTypeArguments()[0] instanceof WildcardType); - } + if (parameterType instanceof ParameterizedType parameterizedType && + (parameterizedType.getRawType().equals(Message.class))) { + return !(parameterizedType.getActualTypeArguments()[0] instanceof WildcardType); } return !parameterType.equals(Message.class); // could be Message without a generic type } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecoverer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecoverer.java index 304e4f7a97..ae84767aa3 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecoverer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecoverer.java @@ -113,7 +113,7 @@ public RepublishMessageRecoverer(AmqpTemplate errorTemplate, String errorExchang public RepublishMessageRecoverer(AmqpTemplate errorTemplate, @Nullable String errorExchange, @Nullable String errorRoutingKey) { - this(errorTemplate, new LiteralExpression(errorExchange), new LiteralExpression(errorRoutingKey)); + this(errorTemplate, new LiteralExpression(errorExchange), new LiteralExpression(errorRoutingKey)); // NOSONAR } /** diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java index f37b56f1eb..6e6deb5bf0 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java @@ -30,6 +30,7 @@ */ public class RabbitMessageReceiverContext extends ReceiverContext { + @Nullable private final String listenerId; private final Message message; From 1f2fd026d5f10b916a713d4ba8efa67c2a0e5566 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 2 Nov 2022 11:55:57 -0400 Subject: [PATCH 190/737] Address Sonar Issue --- .../rabbit/support/micrometer/RabbitMessageReceiverContext.java | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java index 6e6deb5bf0..56eda35198 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java @@ -43,6 +43,7 @@ public RabbitMessageReceiverContext(Message message, @Nullable String listenerId setRemoteServiceName("RabbitMQ"); } + @Nullable public String getListenerId() { return this.listenerId; } From e065b070337deb703d2e58542127a56d024c8a9b Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 2 Nov 2022 13:06:39 -0400 Subject: [PATCH 191/737] Address Sonar Issues --- .../rabbit/listener/AbstractMessageListenerContainer.java | 2 -- .../listener/adapter/DelegatingInvocableHandler.java | 8 ++++---- .../support/micrometer/RabbitMessageReceiverContext.java | 5 +---- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index 3a76dab646..4b4e5c1958 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -159,7 +159,6 @@ public abstract class AbstractMessageListenerContainer extends RabbitAccessor private TransactionAttribute transactionAttribute = new DefaultTransactionAttribute(); - @Nullable private String beanName = "not.a.Spring.bean"; private Executor taskExecutor = new SimpleAsyncTaskExecutor(); @@ -706,7 +705,6 @@ protected RoutingConnectionFactory getRoutingConnectionFactory() { * The 'id' attribute of the listener. * @return the id (or the container bean name if no id set). */ - @Nullable public String getListenerId() { return this.listenerId != null ? this.listenerId : this.beanName; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandler.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandler.java index 6a99f75a2b..12fbf966c7 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandler.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandler.java @@ -136,13 +136,13 @@ public DelegatingInvocableHandler(List handlers, this.resolver = beanExpressionResolver; this.beanExpressionContext = beanExpressionContext; this.validator = validator == null ? null : new PayloadValidator(validator); - boolean asyncReplies; - asyncReplies = defaultHandler != null && isAsyncReply(defaultHandler); + boolean asyncRepl; + asyncRepl = defaultHandler != null && isAsyncReply(defaultHandler); Iterator iterator = handlers.iterator(); while (iterator.hasNext()) { - asyncReplies |= isAsyncReply(iterator.next()); + asyncRepl |= isAsyncReply(iterator.next()); } - this.asyncReplies = asyncReplies; + this.asyncReplies = asyncRepl; } private boolean isAsyncReply(InvocableHandlerMethod method) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java index 56eda35198..3caebc40a6 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java @@ -17,7 +17,6 @@ package org.springframework.amqp.rabbit.support.micrometer; import org.springframework.amqp.core.Message; -import org.springframework.lang.Nullable; import io.micrometer.observation.transport.ReceiverContext; @@ -30,12 +29,11 @@ */ public class RabbitMessageReceiverContext extends ReceiverContext { - @Nullable private final String listenerId; private final Message message; - public RabbitMessageReceiverContext(Message message, @Nullable String listenerId) { + public RabbitMessageReceiverContext(Message message, String listenerId) { super((carrier, key) -> carrier.getMessageProperties().getHeader(key)); setCarrier(message); this.message = message; @@ -43,7 +41,6 @@ public RabbitMessageReceiverContext(Message message, @Nullable String listenerId setRemoteServiceName("RabbitMQ"); } - @Nullable public String getListenerId() { return this.listenerId; } From 9a55484250a0d06783585656b03db1017d7b83e4 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 2 Nov 2022 13:53:29 -0400 Subject: [PATCH 192/737] Address Sonar Issue --- .../rabbit/listener/AbstractMessageListenerContainer.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index 4b4e5c1958..c75c80740f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -1264,11 +1264,7 @@ public void afterPropertiesSet() { try { if (this.micrometerHolder == null && MICROMETER_PRESENT && this.micrometerEnabled && !this.observationEnabled && this.applicationContext != null) { - String id = getListenerId(); - if (id == null) { - id = "no_id_or_beanName"; - } - this.micrometerHolder = new MicrometerHolder(this.applicationContext, id, + this.micrometerHolder = new MicrometerHolder(this.applicationContext, getListenerId(), this.micrometerTags); } } From 49d35adb8a9306bedf06fff58bfcc54e7ff760a4 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 7 Nov 2022 15:26:35 -0500 Subject: [PATCH 193/737] Upgrade to Jackson 2.14.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 5e0f6687d6..25031170fb 100644 --- a/build.gradle +++ b/build.gradle @@ -49,7 +49,7 @@ ext { googleJsr305Version = '3.0.2' hamcrestVersion = '2.2' hibernateValidationVersion = '7.0.5.Final' - jacksonBomVersion = '2.13.4.20221013' + jacksonBomVersion = '2.14.0' jaywayJsonPathVersion = '2.7.0' junit4Version = '4.13.2' junitJupiterVersion = '5.9.1' From 0359526edc04bc284eba9e565bd58f11fd9be773 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 7 Nov 2022 16:12:04 -0500 Subject: [PATCH 194/737] Upgrade Micrometer Versions --- build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 25031170fb..09dac1e6bd 100644 --- a/build.gradle +++ b/build.gradle @@ -56,9 +56,9 @@ ext { log4jVersion = '2.19.0' logbackVersion = '1.4.4' lz4Version = '1.8.0' - micrometerDocsVersion = '1.0.0-SNAPSHOT' - micrometerVersion = '1.10.0-RC1' - micrometerTracingVersion = '1.0.0-RC1' + micrometerDocsVersion = '1.0.0' + micrometerVersion = '1.10.0' + micrometerTracingVersion = '1.0.0' mockitoVersion = '4.8.0' rabbitmqStreamVersion = '0.8.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.16.0' From 7594c3a24fe36c249d3a037ae22f8d8f920dbff0 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 7 Nov 2022 16:31:51 -0500 Subject: [PATCH 195/737] Fix Plugin Repos --- build.gradle | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 09dac1e6bd..79dde2edaf 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,10 @@ buildscript { repositories { mavenCentral() gradlePluginPortal() - maven { url 'https://repo.spring.io/plugins-release' } + maven { url 'https://repo.spring.io/plugins-release-local' } + if (version.endsWith('SNAPSHOT')) { + maven { url 'https://repo.spring.io/snapshot' } + } } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" From 2b177d9f93b6ec9a92c13ab4955814de6eb97747 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 9 Nov 2022 10:27:40 -0500 Subject: [PATCH 196/737] Add spring-amqp-bom * Fix indentation. * Add bom POM to dist.zip. * Fix bom filename in dist.zip. * Add spring-amqp-bom.txt. --- build.gradle | 49 ++++++++++++++++--- .../publish-maven.gradle | 0 settings.gradle | 1 + spring-amqp-bom/spring-amqp-bom.txt | 1 + 4 files changed, 44 insertions(+), 7 deletions(-) rename publish-maven.gradle => gradle/publish-maven.gradle (100%) create mode 100644 spring-amqp-bom/spring-amqp-bom.txt diff --git a/build.gradle b/build.gradle index 79dde2edaf..212a5f19c0 100644 --- a/build.gradle +++ b/build.gradle @@ -72,6 +72,8 @@ ext { springRetryVersion = '2.0.0-RC1' testContainersVersion = '1.17.5' zstdJniVersion = '1.5.0-2' + + javaProjects = subprojects - project(':spring-amqp-bom') } nohttp { @@ -125,10 +127,10 @@ ext { ] as String[] } -subprojects { subproject -> +configure(javaProjects) { subproject -> apply plugin: 'java-library' apply plugin: 'java' - apply from: "${rootProject.projectDir}/publish-maven.gradle" + apply from: "${rootProject.projectDir}/gradle/publish-maven.gradle" apply plugin: 'eclipse' apply plugin: 'idea' apply plugin: 'project-report' @@ -376,6 +378,33 @@ project('spring-amqp') { } +project('spring-amqp-bom') { + description = 'Spring for RabbitMQ (Bill of Materials)' + + apply plugin: 'java-platform' + apply from: "${rootDir}/gradle/publish-maven.gradle" + + dependencies { + constraints { + javaProjects.sort { "$it.name" }.each { + api it + } + } + } + + publishing { + publications { + mavenJava(MavenPublication) { + from components.javaPlatform + } + } + } + + sonarqube { + skipProject = true + } +} + project('spring-rabbit') { description = 'Spring RabbitMQ Support' @@ -618,11 +647,11 @@ task api(type: Javadoc) { addBooleanOption('Xdoclint:syntax', true) // only check syntax with doclint } - source subprojects.collect { project -> + source javaProjects.collect { project -> project.sourceSets.main.allJava } destinationDir = new File(buildDir, 'api') - classpath = files(subprojects.collect { project -> + classpath = files(javaProjects.collect { project -> project.sourceSets.main.compileClasspath }) } @@ -633,7 +662,7 @@ task schemaZip(type: Zip) { description = "Builds -${archiveClassifier} archive containing all " + "XSDs for deployment at static.springframework.org/schema." - subprojects.each { subproject -> + javaProjects.each { subproject -> def Set files = new HashSet() def Properties schemas = new Properties(); def shortName = subproject.name.replaceFirst("${rootProject.name}-", '') @@ -717,13 +746,19 @@ task distZip(type: Zip, dependsOn: [docsZip, schemaZip]) { into "${baseDir}/schema" } - subprojects.each { subproject -> + javaProjects.each { subproject -> into ("${baseDir}/libs") { from subproject.jar from subproject.sourcesJar from subproject.javadocJar } } + + from(project(':spring-amqp-bom').generatePomFileForMavenJavaPublication) { + into "${baseDir}/libs" + rename 'pom-default.xml', "spring-amqp-bom-${project.version}.xml" + } + } // Create an optional "with dependencies" distribution. @@ -769,7 +804,7 @@ task dist(dependsOn: assemble) { description = 'Builds -dist, -docs and -schema distribution archives.' } -apply from: "${rootProject.projectDir}/publish-maven.gradle" +apply from: "${rootProject.projectDir}/gradle/publish-maven.gradle" publishing { publications { diff --git a/publish-maven.gradle b/gradle/publish-maven.gradle similarity index 100% rename from publish-maven.gradle rename to gradle/publish-maven.gradle diff --git a/settings.gradle b/settings.gradle index 5d950cd7c3..6be268ea89 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,7 @@ rootProject.name = 'spring-amqp-dist' include 'spring-amqp' +include 'spring-amqp-bom' include 'spring-rabbit' include 'spring-rabbit-stream' include 'spring-rabbit-junit' diff --git a/spring-amqp-bom/spring-amqp-bom.txt b/spring-amqp-bom/spring-amqp-bom.txt new file mode 100644 index 0000000000..9bf4012144 --- /dev/null +++ b/spring-amqp-bom/spring-amqp-bom.txt @@ -0,0 +1 @@ +This meta-project is used to generate a bill-of-materials POM that contains the other projects in a dependencyManagement section. From e19be2e20612e1b159cd8fd2da5bb0eba68661f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Montalv=C3=A3o=20Marques?= <9379664+GonMMarques@users.noreply.github.com> Date: Mon, 14 Nov 2022 13:23:26 +0000 Subject: [PATCH 197/737] Fix typo in amqp.adoc --- src/reference/asciidoc/amqp.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index c015048056..89da731898 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -3566,7 +3566,7 @@ public void consumerBatch3(List strings) { * the first is called with the raw, unconverted `org.springframework.amqp.core.Message` s received. * the second is called with the `org.springframework.messaging.Message` s with converted payloads and mapped headers/properties. -* the third is called with the converted payloads, with no access to headers/properteis. +* the third is called with the converted payloads, with no access to headers/properties. 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. From d8f78ee29125dc3916c226a73dc1bbe5b8ef5487 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 15 Nov 2022 12:57:09 -0500 Subject: [PATCH 198/737] GH-1533: Template Receive with Consumer Args Resolves https://github.com/spring-projects/spring-amqp/issues/1533 Allow setting consumer arguments when using non-zero receive timeouts using the `RabbitTemplate`. **cherry-pick to 2.4.x** --- .../amqp/rabbit/core/RabbitTemplate.java | 31 ++++++++++++++++- .../core/RabbitTemplateIntegrationTests.java | 8 +++-- .../amqp/rabbit/core/RabbitTemplateTests.java | 33 +++++++++++++++++++ src/reference/asciidoc/amqp.adoc | 5 ++- 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java index 7b4efd9e80..9cacab8731 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java @@ -206,6 +206,8 @@ public class RabbitTemplate extends RabbitAccessor // NOSONAR type line count private final AtomicInteger containerInstance = new AtomicInteger(); + private final Map consumerArgs = new HashMap<>(); + private ApplicationContext applicationContext; private String exchange = DEFAULT_EXCHANGE; @@ -954,6 +956,33 @@ public int getUnconfirmedCount() { .sum(); } + /** + * When using receive methods with a non-zero timeout, a + * {@link com.rabbitmq.client.Consumer} is created to receive the message. Use this + * property to add arguments to the consumer (e.g. {@code x-priority}). + * @param arg the argument name to pass into the {@code basicConsume} operation. + * @param value the argument value to pass into the {@code basicConsume} operation. + * @since 2.4.8 + * @see #removeConsumerArg(String) + */ + public void addConsumerArg(String arg, Object value) { + this.consumerArgs.put(arg, value); + } + + /** + * When using receive methods with a non-zero timeout, a + * {@link com.rabbitmq.client.Consumer} is created to receive the message. Use this + * method to remove an argument from those passed into the {@code basicConsume} + * operation. + * @param arg the argument name. + * @return the previous value. + * @since 2.4.8 + * @see #addConsumerArg(String, Object) + */ + public Object removeConsumerArg(String arg) { + return this.consumerArgs.remove(arg); + } + @Override public void start() { doStart(); @@ -2754,7 +2783,7 @@ public void handleDelivery(String consumerTag, Envelope envelope, BasicPropertie } }; - channel.basicConsume(queueName, consumer); + channel.basicConsume(queueName, false, this.consumerArgs, consumer); if (!latch.await(timeoutMillis, TimeUnit.MILLISECONDS)) { if (channel instanceof ChannelProxy proxy) { proxy.getTargetChannel().close(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateIntegrationTests.java index 6dd054befc..647e931b26 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -335,8 +335,10 @@ class MockChannel extends PublisherCallbackChannelImpl { } @Override - public String basicConsume(String queue, Consumer callback) throws IOException { - return super.basicConsume(queue, new MockConsumer(callback)); + public String basicConsume(String queue, boolean autoAck, Map args, Consumer callback) + throws IOException { + + return super.basicConsume(queue, autoAck, args, new MockConsumer(callback)); } } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateTests.java index 7951a40611..2ceafba40c 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateTests.java @@ -25,6 +25,7 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willAnswer; @@ -49,6 +50,7 @@ import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.springframework.amqp.AmqpAuthenticationException; @@ -636,6 +638,37 @@ void resourcesClearedAfterTxFailsWithSync() throws IOException, TimeoutException ConnectionFactoryUtils.enableAfterCompletionFailureCapture(false); } + @Test + void consumerArgs() throws Exception { + ConnectionFactory mockConnectionFactory = mock(ConnectionFactory.class); + Connection mockConnection = mock(Connection.class); + Channel mockChannel = mock(Channel.class); + + given(mockConnectionFactory.newConnection(any(ExecutorService.class), anyString())).willReturn(mockConnection); + given(mockConnection.isOpen()).willReturn(true); + given(mockConnection.createChannel()).willReturn(mockChannel); + willAnswer(inv -> { + Consumer consumer = inv.getArgument(3); + consumer.handleConsumeOk("tag"); + return null; + }).given(mockChannel).basicConsume(any(), anyBoolean(), anyMap(), any()); + + SingleConnectionFactory connectionFactory = new SingleConnectionFactory(mockConnectionFactory); + connectionFactory.setExecutor(mock(ExecutorService.class)); + RabbitTemplate template = new RabbitTemplate(connectionFactory); + assertThat(template.receive("foo", 1)).isNull(); + @SuppressWarnings("unchecked") + ArgumentCaptor> argsCaptor = ArgumentCaptor.forClass(Map.class); + verify(mockChannel).basicConsume(eq("foo"), eq(false), argsCaptor.capture(), any()); + assertThat(argsCaptor.getValue()).isEmpty(); + template.addConsumerArg("x-priority", 10); + assertThat(template.receive("foo", 1)).isNull(); + assertThat(argsCaptor.getValue()).containsEntry("x-priority", 10); + assertThat(template.removeConsumerArg("x-priority")).isEqualTo(10); + assertThat(template.receive("foo", 1)).isNull(); + assertThat(argsCaptor.getValue()).isEmpty(); + } + @SuppressWarnings("serial") private class TestTransactionManager extends AbstractPlatformTransactionManager { diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index 89da731898..125190c106 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -1886,11 +1886,14 @@ By default, if no message is available, `null` is returned immediately. There is no blocking. Starting with version 1.5, 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 let the timeout be passed in on each call. +Version 1.6 introduced variants of the `receive` methods that allows the timeout be passed in on each call. CAUTION: 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)`. + There are four simple `receive` methods available. As with the `Exchange` on the sending side, there is a method that requires that a default queue property has been set directly on the template itself, and there is a method that accepts a queue parameter at runtime. From ba4ef9bc8cd60c0093b16c3f95986fa78eb27d42 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 16 Nov 2022 16:52:40 -0500 Subject: [PATCH 199/737] Fix Class Tangle Between `LocalizedQueueConnectionFactory` and `NodeLocator` implementations. * Use AOT conventions for WEB_FLUX_PRESENT. --- .../connection/ConnectionFactoryUtils.java | 16 ++- .../amqp/rabbit/connection/FactoryFinder.java | 37 +++++ .../LocalizedQueueConnectionFactory.java | 129 +----------------- .../amqp/rabbit/connection/NodeLocator.java | 121 ++++++++++++++++ .../connection/RestTemplateNodeLocator.java | 1 - .../rabbit/connection/WebFluxNodeLocator.java | 1 - .../rabbit/connection/NodeLocatorTests.java | 1 - 7 files changed, 174 insertions(+), 132 deletions(-) create mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/FactoryFinder.java create mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/NodeLocator.java diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryUtils.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryUtils.java index 8316518f6b..3c8897821a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryUtils.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-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. @@ -25,6 +25,7 @@ import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import com.rabbitmq.client.Channel; @@ -43,6 +44,10 @@ */ public final class ConnectionFactoryUtils { + private static final boolean WEB_FLUX_PRESENT = + ClassUtils.isPresent("org.springframework.web.reactive.function.client.WebClient", + ConnectionFactoryUtils.class.getClassLoader()); + private static final ThreadLocal COMPLETION_EXCEPTIONS = new ThreadLocal<>(); private static boolean captureAfterCompletionExceptions; @@ -252,6 +257,15 @@ public static Connection createConnection(final ConnectionFactory connectionFact return connectionFactory.createConnection(); } + static NodeLocator nodeLocator() { + if (WEB_FLUX_PRESENT) { + return new WebFluxNodeLocator(); + } + else { + return new RestTemplateNodeLocator(); + } + } + /** * Callback interface for resource creation. Serving as argument for the doGetTransactionalChannel * method. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/FactoryFinder.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/FactoryFinder.java new file mode 100644 index 0000000000..8e26ad9524 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/FactoryFinder.java @@ -0,0 +1,37 @@ +/* + * 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.connection; + +/** + * Callback to determine the connection factory using the provided information. + * + * @author Gary Russell + * @since 2.4.8 + */ +@FunctionalInterface +public interface FactoryFinder { + + /** + * Locate or create a factory. + * @param queueName the queue name. + * @param node the node name. + * @param nodeUri the node URI. + * @return the factory. + */ + ConnectionFactory locate(String queueName, String node, String nodeUri); + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java index ab96518bee..eee82f567e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.connection; -import java.net.URI; -import java.net.URISyntaxException; import java.util.AbstractMap.SimpleImmutableEntry; import java.util.Arrays; import java.util.HashMap; @@ -32,10 +30,8 @@ import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.beans.factory.DisposableBean; import org.springframework.core.io.Resource; -import org.springframework.core.log.LogAccessor; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; /** * A {@link RoutingConnectionFactory} that determines the node on which a queue is located and @@ -55,13 +51,6 @@ */ public class LocalizedQueueConnectionFactory implements ConnectionFactory, RoutingConnectionFactory, DisposableBean { - private static final boolean USING_WEBFLUX; - - static { - USING_WEBFLUX = ClassUtils.isPresent("org.springframework.web.reactive.function.client.WebClient", - LocalizedQueueConnectionFactory.class.getClassLoader()); - } - private final Log logger = LogFactory.getLog(getClass()); private final Map nodeFactories = new HashMap(); @@ -198,12 +187,7 @@ private LocalizedQueueConnectionFactory(ConnectionFactory defaultConnectionFacto this.trustStore = trustStore; this.keyStorePassPhrase = keyStorePassPhrase; this.trustStorePassPhrase = trustStorePassPhrase; - if (USING_WEBFLUX) { - this.nodeLocator = new WebFluxNodeLocator(); - } - else { - this.nodeLocator = new RestTemplateNodeLocator(); - } + this.nodeLocator = ConnectionFactoryUtils.nodeLocator(); } private static Map nodesAddressesToMap(String[] nodes, String[] addresses) { @@ -358,115 +342,4 @@ public void destroy() { resetConnection(); } - /** - * Used to obtain a connection factory for the queue leader. - * - * @param the client type. - * @since 2.4.8 - */ - public interface NodeLocator { - - LogAccessor LOGGER = new LogAccessor(LogFactory.getLog(NodeLocator.class)); - - /** - * Return a connection factory for the leader node for the queue. - * @param adminUris an array of admin URIs. - * @param nodeToAddress a map of node names to node addresses (AMQP). - * @param vhost the vhost. - * @param username the user name. - * @param password the password. - * @param queue the queue name. - * @param factoryFunction an internal function to find or create the factory. - * @return a connection factory, if the leader node was found; null otherwise. - */ - @Nullable - default ConnectionFactory locate(String[] adminUris, Map nodeToAddress, String vhost, - String username, String password, String queue, FactoryFinder factoryFunction) { - - T client = createClient(username, password); - - for (int i = 0; i < adminUris.length; i++) { - String adminUri = adminUris[i]; - if (!adminUri.endsWith("/api/")) { - adminUri += "/api/"; - } - try { - String uri = new URI(adminUri) - .resolve("/api/queues/").toString(); - Map queueInfo = restCall(client, uri, vhost, queue); - if (queueInfo != null) { - String node = (String) queueInfo.get("node"); - if (node != null) { - String nodeUri = nodeToAddress.get(node); - if (nodeUri != null) { - close(client); - return factoryFunction.locate(queue, node, nodeUri); - } - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("No match for node: " + node); - } - } - } - else { - throw new AmqpException("Admin returned null QueueInfo"); - } - } - catch (Exception e) { - LOGGER.warn("Failed to determine queue location for: " + queue + " at: " + - adminUri + ": " + e.getMessage()); - } - } - LOGGER.warn("Failed to determine queue location for: " + queue + ", using default connection factory"); - close(client); - return null; - } - - /** - * Create a client for subsequent use. - * @param userName the user name. - * @param password the password. - * @return the client. - */ - T createClient(String userName, String password); - - /** - * Close the client. - * @param client the client. - */ - default void close(T client) { - } - - /** - * Retrieve a map of queue properties using the RabbitMQ Management REST API. - * @param client the client. - * @param baseUri the base uri. - * @param vhost the virtual host. - * @param queue the queue name. - * @return the map of queue properties. - * @throws URISyntaxException if the syntax is bad. - */ - @Nullable - Map restCall(T client, String baseUri, String vhost, String queue) - throws URISyntaxException; - - } - - /** - * Callback to determine the connection factory using the provided information. - * @since 2.4.8 - */ - @FunctionalInterface - public interface FactoryFinder { - - /** - * Locate or create a factory. - * @param queueName the queue name. - * @param node the node name. - * @param nodeUri the node URI. - * @return the factory. - */ - ConnectionFactory locate(String queueName, String node, String nodeUri); - - } - } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/NodeLocator.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/NodeLocator.java new file mode 100644 index 0000000000..aa5dfb2331 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/NodeLocator.java @@ -0,0 +1,121 @@ +/* + * 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.connection; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Map; + +import org.apache.commons.logging.LogFactory; + +import org.springframework.amqp.AmqpException; +import org.springframework.core.log.LogAccessor; +import org.springframework.lang.Nullable; + +/** + * Used to obtain a connection factory for the queue leader. + * @param the client type. + * + * @author Gary Russell + * @since 2.4.8 + */ +public interface NodeLocator { + + LogAccessor LOGGER = new LogAccessor(LogFactory.getLog(NodeLocator.class)); + + /** + * Return a connection factory for the leader node for the queue. + * @param adminUris an array of admin URIs. + * @param nodeToAddress a map of node names to node addresses (AMQP). + * @param vhost the vhost. + * @param username the user name. + * @param password the password. + * @param queue the queue name. + * @param factoryFunction an internal function to find or create the factory. + * @return a connection factory, if the leader node was found; null otherwise. + */ + @Nullable + default ConnectionFactory locate(String[] adminUris, Map nodeToAddress, String vhost, + String username, String password, String queue, FactoryFinder factoryFunction) { + + T client = createClient(username, password); + + for (int i = 0; i < adminUris.length; i++) { + String adminUri = adminUris[i]; + if (!adminUri.endsWith("/api/")) { + adminUri += "/api/"; + } + try { + String uri = new URI(adminUri) + .resolve("/api/queues/").toString(); + Map queueInfo = restCall(client, uri, vhost, queue); + if (queueInfo != null) { + String node = (String) queueInfo.get("node"); + if (node != null) { + String nodeUri = nodeToAddress.get(node); + if (nodeUri != null) { + close(client); + return factoryFunction.locate(queue, node, nodeUri); + } + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("No match for node: " + node); + } + } + } + else { + throw new AmqpException("Admin returned null QueueInfo"); + } + } + catch (Exception e) { + LOGGER.warn("Failed to determine queue location for: " + queue + " at: " + + adminUri + ": " + e.getMessage()); + } + } + LOGGER.warn("Failed to determine queue location for: " + queue + ", using default connection factory"); + close(client); + return null; + } + + /** + * Create a client for subsequent use. + * @param userName the user name. + * @param password the password. + * @return the client. + */ + T createClient(String userName, String password); + + /** + * Close the client. + * @param client the client. + */ + default void close(T client) { + } + + /** + * Retrieve a map of queue properties using the RabbitMQ Management REST API. + * @param client the client. + * @param baseUri the base uri. + * @param vhost the virtual host. + * @param queue the queue name. + * @return the map of queue properties. + * @throws URISyntaxException if the syntax is bad. + */ + @Nullable + Map restCall(T client, String baseUri, String vhost, String queue) + throws URISyntaxException; + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateNodeLocator.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateNodeLocator.java index be7fcae452..6f1509ca31 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateNodeLocator.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateNodeLocator.java @@ -30,7 +30,6 @@ import org.apache.hc.core5.http.protocol.BasicHttpContext; import org.apache.hc.core5.http.protocol.HttpContext; -import org.springframework.amqp.rabbit.connection.LocalizedQueueConnectionFactory.NodeLocator; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/WebFluxNodeLocator.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/WebFluxNodeLocator.java index 5e00db4ae9..6c10164474 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/WebFluxNodeLocator.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/WebFluxNodeLocator.java @@ -23,7 +23,6 @@ import java.util.HashMap; import java.util.Map; -import org.springframework.amqp.rabbit.connection.LocalizedQueueConnectionFactory.NodeLocator; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.MediaType; import org.springframework.lang.Nullable; diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/NodeLocatorTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/NodeLocatorTests.java index 0fe98fb487..cd802b4231 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/NodeLocatorTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/NodeLocatorTests.java @@ -27,7 +27,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.amqp.rabbit.connection.LocalizedQueueConnectionFactory.NodeLocator; import org.springframework.lang.Nullable; /** From ce78ad3728695d06d3fe6b7e3506b5010cbf58f9 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 17 Nov 2022 14:04:46 -0500 Subject: [PATCH 200/737] Docs for Native Images --- src/reference/asciidoc/appendix.adoc | 8 ++++++++ src/reference/asciidoc/whats-new.adoc | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/src/reference/asciidoc/appendix.adoc b/src/reference/asciidoc/appendix.adoc index e18b92e413..7f8c82f5a4 100644 --- a/src/reference/asciidoc/appendix.adoc +++ b/src/reference/asciidoc/appendix.adoc @@ -8,6 +8,14 @@ include::../docs/generated/spans.adoc[] include::../docs/generated/conventions.adoc[] +[appendix] +[[native-images]] +== Native Images + +https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aot[Spring AOT] native hints are provided to assist in developing native images for Spring applications that use Spring AMQP. + +Some examples can be seen in the https://github.com/spring-projects/spring-aot-smoke-tests/tree/main/integration[`spring-aot-smoke-tests` GitHub repository]. + [appendix] [[change-history]] == Change History diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index a55a2d40c5..1e5cd57eeb 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -16,6 +16,12 @@ The remoting feature (using RMI) is no longer supported. Enabling observation for timers and tracing using Micrometer is now supported. See <> for more information. +[[x30-Native]] +==== Native Images + +Support for creating native images is provided. +See <> for more information. + ==== AsyncRabbitTemplate IMPORTANT: The `AsyncRabbitTemplate` now returns `CompletableFuture` s instead of `ListenableFuture` s. From 2351e650db9d38941899050f238b220d4df5e028 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 21 Nov 2022 10:02:33 -0500 Subject: [PATCH 201/737] Upgrade Versions; Prepare for Release --- build.gradle | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index 212a5f19c0..687dd1a8b1 100644 --- a/build.gradle +++ b/build.gradle @@ -51,7 +51,7 @@ ext { commonsPoolVersion = '2.11.1' googleJsr305Version = '3.0.2' hamcrestVersion = '2.2' - hibernateValidationVersion = '7.0.5.Final' + hibernateValidationVersion = '8.0.0.Final' jacksonBomVersion = '2.14.0' jaywayJsonPathVersion = '2.7.0' junit4Version = '4.13.2' @@ -60,17 +60,17 @@ ext { logbackVersion = '1.4.4' lz4Version = '1.8.0' micrometerDocsVersion = '1.0.0' - micrometerVersion = '1.10.0' + micrometerVersion = '1.10.1' micrometerTracingVersion = '1.0.0' - mockitoVersion = '4.8.0' + mockitoVersion = '4.8.1' rabbitmqStreamVersion = '0.8.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.16.0' - reactorVersion = '2022.0.0-RC1' + reactorVersion = '2022.0.0' snappyVersion = '1.1.8.4' - springDataVersion = '2022.0.0-RC1' - springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.0-RC1' - springRetryVersion = '2.0.0-RC1' - testContainersVersion = '1.17.5' + springDataVersion = '2022.0.0' + springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.0' + springRetryVersion = '2.0.0' + testContainersVersion = '1.17.6' zstdJniVersion = '1.5.0-2' javaProjects = subprojects - project(':spring-amqp-bom') From 0ac21dd4b8e83e9eff386cfb1e39e88f9b2ec554 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 21 Nov 2022 10:25:06 -0500 Subject: [PATCH 202/737] Remove RestTemplate.close() Call https://github.com/spring-projects/spring-framework/issues/29370 --- .../rabbit/connection/RestTemplateNodeLocator.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateNodeLocator.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateNodeLocator.java index 6f1509ca31..f736ae6f58 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateNodeLocator.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateNodeLocator.java @@ -16,7 +16,6 @@ package org.springframework.amqp.rabbit.connection; -import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; @@ -84,13 +83,4 @@ protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri) { return response.getStatusCode().equals(HttpStatus.OK) ? response.getBody() : null; } - @Override - public void close(RestTemplateHolder client) { - try { - client.template.close(); - } - catch (IOException e) { - } - } - } From abdb4c0442df31944d794859724de1899d24963e Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 21 Nov 2022 15:50:32 +0000 Subject: [PATCH 203/737] [artifactory-release] Release version 3.0.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 2477c6a3c5..35ddad50b0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.0-SNAPSHOT +version=3.0.0 org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true From 3fa56c0d0cfdbf9c9b2da6fc216e604090ceb5d3 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 21 Nov 2022 15:50:34 +0000 Subject: [PATCH 204/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 35ddad50b0..b7aace098e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.0 +version=3.0.1-SNAPSHOT org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true From 9a69e3a354fb7d98942986e81ad4976b74745c33 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Fri, 25 Nov 2022 13:09:21 -0500 Subject: [PATCH 205/737] GH-1539: MessageProperties.expiration Javadoc Resolves https://github.com/spring-projects/spring-amqp/issues/1539 --- .../amqp/core/MessageProperties.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java index 6f955d381e..ba8704fca1 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java @@ -315,13 +315,22 @@ public void setReceivedDeliveryMode(MessageDeliveryMode receivedDeliveryMode) { this.receivedDeliveryMode = receivedDeliveryMode; } - // why not a Date or long? + /** + * Set the message expiration. This is a String property per the AMQP 0.9.1 spec. For + * RabbitMQ, this is a String representation of the message time to live in + * milliseconds. + * @param expiration the expiration. + */ public void setExpiration(String expiration) { this.expiration = expiration; } - // NOTE qpid Java broker qpid 0.8/1.0 .NET: is a long. - // 0.8 Spec has: expiration (shortstr) + /** + * Get the message expiration. This is a String property per the AMQP 0.9.1 spec. For + * RabbitMQ, this is a String representation of the message time to live in + * milliseconds. + * @return the expiration. + */ public String getExpiration() { return this.expiration; } From 97df895c8e5415856be46f4317954774ff6dbde8 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 28 Nov 2022 12:15:33 -0500 Subject: [PATCH 206/737] GH-1541: Fix Container Docs Resolves https://github.com/spring-projects/spring-amqp/issues/1541 Containers can have zero queues. --- src/reference/asciidoc/amqp.adoc | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index 125190c106..091bff6970 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -6588,15 +6588,12 @@ When using exclusive consumers, other containers try to consume from the queues Version 1.3 introduced a number of improvements for handling multiple queues in a listener container. -The container must be configured to listen on at least one queue. -This was the case previously, too, but now queues can be added and removed at runtime. -The container recycles (cancels and re-creates) the consumers when any pre-fetched messages have been processed. +Container can be initially configured to listen on zero queues. +Queues can be added and removed at runtime. +The `SimpleMessageListenerContainer` recycles (cancels and re-creates) all consumers when any pre-fetched messages have been processed. +The `DirectMessageListenerContainer` creates/cancels individual consumer(s) for each queue without affecting consumers on other queues. See the https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.html[Javadoc] for the `addQueues`, `addQueueNames`, `removeQueues` and `removeQueueNames` methods. -When removing queues, at least one queue must remain. -A consumer now starts if any of its queues are available. -Previously, the container would stop if any queues were unavailable. -Now, this is only the case if none of the queues are available. If not all queues are available, the container tries to passively declare (and consume from) the missing queues every 60 seconds. Also, if a consumer receives a cancel from the broker (for example, if a queue is deleted) the consumer tries to recover, and the recovered consumer continues to process messages from any other configured queues. From c58e86f271b3bf0a0813f5680931e40b5c217ae2 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 1 Dec 2022 11:09:11 -0500 Subject: [PATCH 207/737] AMQP-52:Remove Obsolete MessageProperties Comments JIRA: https://jira.spring.io/projects/AMQP/issues/AMQP-52 Differences with AMQP 1.0 types - AMQP 1.0 is not supported. --- .../springframework/amqp/core/MessageProperties.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java index ba8704fca1..eba31fb895 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java @@ -169,15 +169,10 @@ public void setTimestamp(Date timestamp) { this.timestamp = timestamp; //NOSONAR } - // NOTE qpid java timestamp is long, presumably can convert to Date. public Date getTimestamp() { return this.timestamp; //NOSONAR } - // NOTE Not forward compatible with qpid 1.0 .NET - // qpid 0.8 .NET/Java: is a string - // qpid 1.0 .NET: MessageId property on class MessageProperties and is UUID - // There is an 'ID' stored IMessage class and is an int. public void setMessageId(String messageId) { this.messageId = messageId; } @@ -190,9 +185,6 @@ public void setUserId(String userId) { this.userId = userId; } - // NOTE Note forward compatible with qpid 1.0 .NET - // qpid 0.8 .NET/java: is a string - // qpid 1.0 .NET: getUserId is byte[] public String getUserId() { return this.userId; } @@ -218,9 +210,6 @@ public String getAppId() { return this.appId; } - // NOTE not forward compatible with qpid 1.0 .NET - // qpid 0.8 .NET/Java: is a string - // qpid 1.0 .NET: is not present public void setClusterId(String clusterId) { this.clusterId = clusterId; } @@ -233,7 +222,6 @@ public void setType(String type) { this.type = type; } - // NOTE structureType is int in Qpid public String getType() { return this.type; } From c88a6580d9e0143ce9c84da346d4dc49fe4e2ebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Montalv=C3=A3o=20Marques?= <9379664+GonMMarques@users.noreply.github.com> Date: Wed, 14 Dec 2022 14:41:14 +0000 Subject: [PATCH 208/737] Fix typo in amqp.adoc --- src/reference/asciidoc/amqp.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index 091bff6970..77109b16dc 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -3976,7 +3976,7 @@ See <> for important information. ====== Converting To a `Message` When converting to a `Message` from an arbitrary Java Object, the `SimpleMessageConverter` likewise deals with byte arrays, strings, and serializable instances. -It converts each of these to bytes (in the case of byte arrays, there is nothing to convert), and it ses the content-type property accordingly. +It converts each of these to bytes (in the case of byte arrays, there is nothing to convert), and it sets the content-type property accordingly. If the `Object` to be converted does not match one of those types, the `Message` body is null. [[serializer-message-converter]] From 87aa29cd02f4e2f9e4dcbc5d7261f8df032921f4 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 9 Jan 2023 15:23:27 -0500 Subject: [PATCH 209/737] GH-1550: Fix Mono Return Type Detection Resolves https://github.com/spring-projects/spring-amqp/issues/1550 **cherry-pick to 2.4.x** --- .../amqp/rabbit/listener/adapter/MonoHandler.java | 6 +++++- .../amqp/rabbit/annotation/AsyncListenerTests.java | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MonoHandler.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MonoHandler.java index f594e44eab..2030970803 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MonoHandler.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MonoHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2023 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. @@ -34,6 +34,10 @@ static boolean isMono(Object result) { return result instanceof Mono; } + static boolean isMono(Class resultType) { + return Mono.class.isAssignableFrom(resultType); + } + @SuppressWarnings("unchecked") static void subscribe(Object returnValue, Consumer success, Consumer failure, Runnable completeConsumer) { diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AsyncListenerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AsyncListenerTests.java index 459cdb070e..9ef3250623 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AsyncListenerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AsyncListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2022 the original author or authors. + * Copyright 2018-2023 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. @@ -104,7 +104,7 @@ public class AsyncListenerTests { private RabbitListenerEndpointRegistry registry; @Test - public void testAsyncListener() throws Exception { + public void testAsyncListener(@Autowired RabbitListenerEndpointRegistry registry) throws Exception { assertThat(this.rabbitTemplate.convertSendAndReceive(this.queue1.getName(), "foo")).isEqualTo("FOO"); RabbitConverterFuture future = this.asyncTemplate.convertSendAndReceive(this.queue1.getName(), "foo"); assertThat(future.get(10, TimeUnit.SECONDS)).isEqualTo("FOO"); @@ -118,6 +118,10 @@ public void testAsyncListener() throws Exception { assertThat(this.config.contentTypeId).isEqualTo("java.lang.String"); this.rabbitTemplate.convertAndSend(this.queue4.getName(), "foo"); assertThat(listener.latch4.await(10, TimeUnit.SECONDS)); + assertThat(TestUtils.getPropertyValue(registry.getListenerContainer("foo"), "asyncReplies", Boolean.class)) + .isTrue(); + assertThat(TestUtils.getPropertyValue(registry.getListenerContainer("bar"), "asyncReplies", Boolean.class)) + .isTrue(); } @Test From 41741f2e68e96a184b6d721135571e72d91f61ad Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 9 Jan 2023 16:29:10 -0500 Subject: [PATCH 210/737] Fix Testcontainer Tests With No Docker Running Disable the tests, if Docker is not available. --- build.gradle | 11 +++++++---- .../amqp/rabbit/junit/AbstractTestContainerTests.java | 4 +++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 687dd1a8b1..8bfbf4f0b2 100644 --- a/build.gradle +++ b/build.gradle @@ -70,7 +70,7 @@ ext { springDataVersion = '2022.0.0' springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.0' springRetryVersion = '2.0.0' - testContainersVersion = '1.17.6' + testcontainersVersion = '1.17.6' zstdJniVersion = '1.5.0-2' javaProjects = subprojects - project(':spring-amqp-bom') @@ -104,6 +104,7 @@ allprojects { mavenBom "org.springframework.data:spring-data-bom:$springDataVersion" mavenBom "io.micrometer:micrometer-bom:$micrometerVersion" mavenBom "io.micrometer:micrometer-tracing-bom:$micrometerTracingVersion" + mavenBom "org.testcontainers:testcontainers-bom:$testcontainersVersion" } } @@ -443,7 +444,8 @@ project('spring-rabbit') { testImplementation 'io.micrometer:micrometer-tracing-bridge-brave' testImplementation 'io.micrometer:micrometer-tracing-test' testImplementation 'io.micrometer:micrometer-tracing-integration-test' - testImplementation "org.testcontainers:rabbitmq:$testContainersVersion" + testImplementation "org.testcontainers:rabbitmq" + testImplementation 'org.testcontainers:junit-jupiter' testRuntimeOnly 'com.fasterxml.jackson.core:jackson-core' testRuntimeOnly 'com.fasterxml.jackson.core:jackson-databind' testRuntimeOnly 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' @@ -480,7 +482,7 @@ project('spring-rabbit-stream') { testRuntimeOnly "org.xerial.snappy:snappy-java:$snappyVersion" testRuntimeOnly "org.lz4:lz4-java:$lz4Version" testRuntimeOnly "com.github.luben:zstd-jni:$zstdJniVersion" - testImplementation "org.testcontainers:rabbitmq:$testContainersVersion" + testImplementation "org.testcontainers:rabbitmq" testImplementation "org.apache.logging.log4j:log4j-slf4j-impl:$log4jVersion" testImplementation 'org.springframework:spring-webflux' } @@ -501,7 +503,8 @@ project('spring-rabbit-junit') { api 'org.springframework:spring-web' api 'org.junit.jupiter:junit-jupiter-api' api "org.assertj:assertj-core:$assertjVersion" - optionalApi "org.testcontainers:rabbitmq:$testContainersVersion" + optionalApi "org.testcontainers:rabbitmq" + optionalApi "org.testcontainers:junit-jupiter" optionalApi "ch.qos.logback:logback-classic:$logbackVersion" optionalApi 'org.apache.logging.log4j:log4j-core' compileOnly 'org.apiguardian:apiguardian-api:1.0.0' diff --git a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/AbstractTestContainerTests.java b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/AbstractTestContainerTests.java index 12c49e6399..f3246d9ab5 100644 --- a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/AbstractTestContainerTests.java +++ b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/AbstractTestContainerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 the original author or authors. + * Copyright 2021-2023 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. @@ -19,6 +19,7 @@ import java.time.Duration; import org.testcontainers.containers.RabbitMQContainer; +import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; /** @@ -26,6 +27,7 @@ * @since 2.4 * */ +@Testcontainers(disabledWithoutDocker = true) public abstract class AbstractTestContainerTests { protected static final RabbitMQContainer RABBITMQ; From 83cd6a631ade64a56ef6756565bb14c1b6d9f6b4 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 9 Jan 2023 16:29:10 -0500 Subject: [PATCH 211/737] Fix Testcontainer Tests With No Docker Running Need junit-jupiter in s-r-stream too. --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 8bfbf4f0b2..0e048af1cd 100644 --- a/build.gradle +++ b/build.gradle @@ -483,6 +483,7 @@ project('spring-rabbit-stream') { testRuntimeOnly "org.lz4:lz4-java:$lz4Version" testRuntimeOnly "com.github.luben:zstd-jni:$zstdJniVersion" testImplementation "org.testcontainers:rabbitmq" + testImplementation "org.testcontainers:junit-jupiter" testImplementation "org.apache.logging.log4j:log4j-slf4j-impl:$log4jVersion" testImplementation 'org.springframework:spring-webflux' } From 1d5d0c29f50cf8685fe156d77894317a83826e15 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 16 Jan 2023 11:33:48 -0500 Subject: [PATCH 212/737] Upgrade Spring, Micrometer, Reactor Versions --- build.gradle | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index 0e048af1cd..949687ef00 100644 --- a/build.gradle +++ b/build.gradle @@ -59,16 +59,16 @@ ext { log4jVersion = '2.19.0' logbackVersion = '1.4.4' lz4Version = '1.8.0' - micrometerDocsVersion = '1.0.0' - micrometerVersion = '1.10.1' - micrometerTracingVersion = '1.0.0' + micrometerDocsVersion = '1.0.1' + micrometerVersion = '1.10.3' + micrometerTracingVersion = '1.0.1' mockitoVersion = '4.8.1' rabbitmqStreamVersion = '0.8.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.16.0' - reactorVersion = '2022.0.0' + reactorVersion = '2022.0.2' snappyVersion = '1.1.8.4' - springDataVersion = '2022.0.0' - springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.0' + springDataVersion = '2022.0.1' + springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.4' springRetryVersion = '2.0.0' testcontainersVersion = '1.17.6' zstdJniVersion = '1.5.0-2' From 2935c785a376fc78333e9ea3e23fa9ad0fa965f2 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Tue, 17 Jan 2023 02:01:06 +0000 Subject: [PATCH 213/737] [artifactory-release] Release version 3.0.1 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index b7aace098e..70207b2639 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.1-SNAPSHOT +version=3.0.1 org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true From ae7ba848bb02a8b21fd2e08fe34b042be68f0a44 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Tue, 17 Jan 2023 02:01:08 +0000 Subject: [PATCH 214/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 70207b2639..63baf06f92 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.1 +version=3.0.2-SNAPSHOT org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true From b3a4b2589e1fd0e2c3a1b04bd8c662c701a8f261 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gonc=CC=A7alo=20Marques?= <9379664+GonMMarques@users.noreply.github.com> Date: Wed, 18 Jan 2023 14:34:41 +0000 Subject: [PATCH 215/737] Fix some typos in documentation --- src/reference/asciidoc/amqp.adoc | 10 +++++----- src/reference/asciidoc/appendix.adoc | 2 +- src/reference/asciidoc/stream.adoc | 2 +- src/reference/asciidoc/testing.adoc | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index 77109b16dc..b8a0c2bdf7 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -1612,7 +1612,7 @@ The second obtains the `username` property from a connection factory bean in the Starting with version 2.0.2, you can set the `usePublisherConnection` property to `true` to use a different connection to that used by listener containers, when possible. This is to avoid consumers being blocked when a producer is blocked for any reason. -The connection factories maintain a second internal connection factory for this purpose; by default it is the same type as the main factory, but can be set explicity if you wish to use a different factory type for publishing. +The connection factories maintain a second internal connection factory for this purpose; by default it is the same type as the main factory, but can be set explicitly if you wish to use a different factory type for publishing. If the rabbit template is running in a transaction started by the listener container, the container's channel is used, regardless of this setting. IMPORTANT: In general, you should not use a `RabbitAdmin` with a template that has this set to `true`. @@ -2598,7 +2598,7 @@ It creates `DirectMessageListenerContainer` instances. NOTE: For information to help you choose between `SimpleRabbitListenerContainerFactory` and `DirectRabbitListenerContainerFactory`, see <>. -Starting wih version 2.2.2, you can provide a `ContainerCustomizer` implementation (as shown above). +Starting with version 2.2.2, you can provide a `ContainerCustomizer` implementation (as shown above). This can be used to further configure the container after it has been created and configured; you can use this, for example, to set properties that are not exposed by the container factory. Version 2.4.8 provides the `CompositeContainerCustomizer` for situations where you wish to apply multiple customizers. @@ -3112,7 +3112,7 @@ public Message processOrder(Order order) { ==== Alternatively, you can use a `MessagePostProcessor` in the `beforeSendReplyMessagePostProcessors` container factory property to add more headers. -Starting with version 2.2.3, the called bean/method is made avaiable in the reply message, which can be used in a message post processor to communicate the information back to the caller: +Starting with version 2.2.3, 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: ==== [source, java] @@ -5338,7 +5338,7 @@ The properties are available as attributes in the namespace, as shown in the fol NOTE: By default, the `auto-declare` attribute is `true` and, if the `declared-by` is not supplied (or is empty), then all `RabbitAdmin` instances declare the object (as long as the admin's `auto-startup` attribute is `true`, the default, and the admin's `explicit-declarations-only` attribute is false). Similarly, you can use Java-based `@Configuration` to achieve the same effect. -In the following example, the components are declared by `admin1` but not by`admin2`: +In the following example, the components are declared by `admin1` but not by `admin2`: ==== [source,java] @@ -5973,7 +5973,7 @@ a|image::images/tickmark.png[] (connection-factory) |A reference to the `ConnectionFactory`. -When configuring byusing the XML namespace, the default referenced bean name is `rabbitConnectionFactory`. +When configuring by using the XML namespace, the default referenced bean name is `rabbitConnectionFactory`. a|image::images/tickmark.png[] a|image::images/tickmark.png[] diff --git a/src/reference/asciidoc/appendix.adoc b/src/reference/asciidoc/appendix.adoc index 7f8c82f5a4..993fb899ec 100644 --- a/src/reference/asciidoc/appendix.adoc +++ b/src/reference/asciidoc/appendix.adoc @@ -924,7 +924,7 @@ See <> for more information. ====== Mandatory with `sendAndReceive` Methods When the `mandatory` flag is set when using the `sendAndReceive` and `convertSendAndReceive` methods, the calling thread -throws an `AmqpMessageReturnedException` if the request message cannot be deliverted. +throws an `AmqpMessageReturnedException` if the request message cannot be delivered. See <> for more information. ====== Improper Reply Listener Configuration diff --git a/src/reference/asciidoc/stream.adoc b/src/reference/asciidoc/stream.adoc index 01a72ccdd3..dda312392a 100644 --- a/src/reference/asciidoc/stream.adoc +++ b/src/reference/asciidoc/stream.adoc @@ -61,7 +61,7 @@ The `MessageConverter` is used in the `convertAndSend` methods to convert the ob The `StreamMessageConverter` is used to convert from a Spring AMQP `Message` to a native stream `Message`. -You can also send native stream `Message` s directly; with the `messageBuilder()` method provding access to the `Producer` 's message builder. +You can also send native stream `Message` s directly; with the `messageBuilder()` method providing access to the `Producer` 's message builder. The `ProducerCustomizer` provides a mechanism to customize the producer before it is built. diff --git a/src/reference/asciidoc/testing.adoc b/src/reference/asciidoc/testing.adoc index 5a3c314fc6..feec38c865 100644 --- a/src/reference/asciidoc/testing.adoc +++ b/src/reference/asciidoc/testing.adoc @@ -21,7 +21,7 @@ This is not necessary when using, for example `@SpringBootTest` since Spring Boo Beans that are registered are: -* `CachingConnectionFactory` (`autoConnectionFactory`). If `@RabbitEnabled` is present, its connectionn factory is used. +* `CachingConnectionFactory` (`autoConnectionFactory`). If `@RabbitEnabled` is present, its connection factory is used. * `RabbitTemplate` (`autoRabbitTemplate`) * `RabbitAdmin` (`autoRabbitAdmin`) * `RabbitListenerContainerFactory` (`autoContainerFactory`) From 931896d4f03ce4804c1263af74cdc2dca6f2d7d2 Mon Sep 17 00:00:00 2001 From: Tim Bq Date: Tue, 7 Feb 2023 15:53:47 +0100 Subject: [PATCH 216/737] GH-1561: Alwways run stop callback Fixes https://github.com/spring-projects/spring-amqp/issues/1561 * GH-1561 run callback when container is stopping for abort too * GH-1561 add author information **Cherry-pick to `2.4.x`** --- .../SimpleMessageListenerContainer.java | 15 ++++++++--- .../SimpleMessageListenerContainerTests.java | 26 ++++++++++++++++++- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java index 023a7a5031..9f93320242 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -79,6 +79,7 @@ * @author Alex Panchenko * @author Mat Jaggard * @author Yansong Ren + * @author Tim Bourquin * * @since 1.0 */ @@ -622,6 +623,7 @@ private void shutdownAndWaitOrCallback(@Nullable Runnable callback) { Thread thread = this.containerStoppingForAbort.get(); if (thread != null && !thread.equals(Thread.currentThread())) { logger.info("Shutdown ignored - container is stopping due to an aborted consumer"); + runCallbackIfNotNull(callback); return; } @@ -641,6 +643,7 @@ private void shutdownAndWaitOrCallback(@Nullable Runnable callback) { } else { logger.info("Shutdown ignored - container is already stopped"); + runCallbackIfNotNull(callback); return; } } @@ -674,9 +677,7 @@ private void shutdownAndWaitOrCallback(@Nullable Runnable callback) { this.cancellationLock.deactivate(); } - if (callback != null) { - callback.run(); - } + runCallbackIfNotNull(callback); }; if (callback == null) { awaitShutdown.run(); @@ -686,6 +687,12 @@ private void shutdownAndWaitOrCallback(@Nullable Runnable callback) { } } + private void runCallbackIfNotNull(@Nullable Runnable callback) { + if (callback != null) { + callback.run(); + } + } + private boolean isActive(BlockingQueueConsumer consumer) { boolean consumerActive; synchronized (this.consumersMonitor) { diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java index 1b7d7d1229..3cabcfe319 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -108,6 +108,7 @@ * @author Artem Bilan * @author Mohammad Hewedy * @author Yansong Ren + * @author Tim Bourquin */ public class SimpleMessageListenerContainerTests { @@ -431,6 +432,29 @@ protected void setUpMockCancel(Channel channel, final List consumers) }).given(channel).basicCancel(anyString()); } + @Test + public void testCallbackIsRunOnStopAlsoWhenNoConsumerIsActive() throws InterruptedException { + ConnectionFactory connectionFactory = mock(ConnectionFactory.class); + + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); + + final CountDownLatch countDownLatch = new CountDownLatch(1); + container.stop(countDownLatch::countDown); + assertThat(countDownLatch.await(100, TimeUnit.MILLISECONDS)).isTrue(); + } + + @Test + public void testCallbackIsRunOnStopAlsoWhenContainerIsStoppingForAbort() throws InterruptedException { + ConnectionFactory connectionFactory = mock(ConnectionFactory.class); + + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); + ReflectionTestUtils.setField(container, "containerStoppingForAbort", new AtomicReference<>(new Thread())); + + final CountDownLatch countDownLatch = new CountDownLatch(1); + container.stop(countDownLatch::countDown); + assertThat(countDownLatch.await(100, TimeUnit.MILLISECONDS)).isTrue(); + } + @Test public void testWithConnectionPerListenerThread() throws Exception { com.rabbitmq.client.ConnectionFactory mockConnectionFactory = From 2f48c79acca6f2e20b3fcea8dd586c23be818d08 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 7 Feb 2023 13:02:37 -0500 Subject: [PATCH 217/737] GH-1560: Caching CF toString() Improvements Resolves https://github.com/spring-projects/spring-amqp/issues/1560 Use address resolution hierarchy to determine destination host(s). --- .../connection/AbstractConnectionFactory.java | 8 ++++++- .../connection/CachingConnectionFactory.java | 20 +++++++++++++--- .../rabbit/connection/ConnectionFactory.java | 3 ++- .../CachingConnectionFactoryTests.java | 24 ++++++++++++++++++- 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java index 3bc3bf9510..44a5bd270e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -303,6 +303,7 @@ public void setUri(String uri) { } @Override + @Nullable public String getHost() { return this.rabbitConnectionFactory.getHost(); } @@ -354,6 +355,11 @@ public synchronized void setAddresses(String addresses) { this.addresses = null; } + @Nullable + protected List
getAddresses() throws IOException { + return this.addressResolver != null ? this.addressResolver.getAddresses() : this.addresses; + } + /** * A composite connection listener to be used by subclasses when creating and closing connections. * diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java index 1397d1d472..1502fca5b6 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -58,6 +58,7 @@ import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; +import com.rabbitmq.client.Address; import com.rabbitmq.client.AlreadyClosedException; import com.rabbitmq.client.BlockedListener; import com.rabbitmq.client.Channel; @@ -1003,8 +1004,21 @@ protected ExecutorService getChannelsExecutor() { @Override public String toString() { - return "CachingConnectionFactory [channelCacheSize=" + this.channelCacheSize + ", host=" + getHost() - + ", port=" + getPort() + ", active=" + this.active + String host = getHost(); + int port = getPort(); + List
addresses = null; + try { + addresses = getAddresses(); + } + catch (IOException ex) { + host = "AddressResolver threw exception: " + ex.getMessage(); + } + return "CachingConnectionFactory [channelCacheSize=" + this.channelCacheSize + + (addresses != null + ? ", addresses=" + addresses + : (host != null ? ", host=" + host : "") + + (port > 0 ? ", port=" + port : "")) + + ", active=" + this.active + " " + super.toString() + "]"; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactory.java index 2f4323b540..d435f9b3dc 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -33,6 +33,7 @@ public interface ConnectionFactory { Connection createConnection() throws AmqpException; + @Nullable String getHost(); int getPort(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java index 5126ea9a17..5d8165ac88 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -104,6 +104,28 @@ protected AbstractConnectionFactory createConnectionFactory(ConnectionFactory co return ccf; } + @Test + void stringRepresentation() { + CachingConnectionFactory ccf = new CachingConnectionFactory("someHost", 1234); + assertThat(ccf.toString()).contains(", host=someHost, port=1234") + .doesNotContain("addresses"); + ccf.setAddresses("h1:1234,h2:1235"); + assertThat(ccf.toString()).contains(", addresses=[h1:1234, h2:1235]") + .doesNotContain("host") + .doesNotContain("port"); + ccf.setAddressResolver(() -> List.of(new Address("h3", 1236), new Address("h4", 1237))); + assertThat(ccf.toString()).contains(", addresses=[h3:1236, h4:1237]") + .doesNotContain("host") + .doesNotContain("port"); + ccf.setAddressResolver(() -> { + throw new IOException("test"); + }); + ccf.setPort(0); + assertThat(ccf.toString()).contains(", host=AddressResolver threw exception: test") + .doesNotContain("addresses") + .doesNotContain("port"); + } + @Test public void testWithConnectionFactoryDefaults() throws Exception { com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class); From 3af693506bd4fefae1c161da4ed0550b608d8773 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 15 Feb 2023 11:27:20 -0500 Subject: [PATCH 218/737] Fix Sonar Issue Inconsistent synch. --- .../amqp/rabbit/connection/AbstractConnectionFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java index 44a5bd270e..e6ebec087f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java @@ -356,7 +356,7 @@ public synchronized void setAddresses(String addresses) { } @Nullable - protected List
getAddresses() throws IOException { + protected synchronized List
getAddresses() throws IOException { return this.addressResolver != null ? this.addressResolver.getAddresses() : this.addresses; } From 508484c963abd79dc93ea179c8e591bea0a8fb01 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 21 Feb 2023 09:57:19 -0500 Subject: [PATCH 219/737] Upgrade Versions; Prepare for Release --- build.gradle | 10 +++++----- .../amqp/core/Base64UrlNamingStrategy.java | 6 +++--- .../amqp/rabbit/junit/BrokerRunningSupport.java | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/build.gradle b/build.gradle index 949687ef00..6f0b17e2ca 100644 --- a/build.gradle +++ b/build.gradle @@ -52,7 +52,7 @@ ext { googleJsr305Version = '3.0.2' hamcrestVersion = '2.2' hibernateValidationVersion = '8.0.0.Final' - jacksonBomVersion = '2.14.0' + jacksonBomVersion = '2.14.2' jaywayJsonPathVersion = '2.7.0' junit4Version = '4.13.2' junitJupiterVersion = '5.9.1' @@ -60,15 +60,15 @@ ext { logbackVersion = '1.4.4' lz4Version = '1.8.0' micrometerDocsVersion = '1.0.1' - micrometerVersion = '1.10.3' - micrometerTracingVersion = '1.0.1' + micrometerVersion = '1.10.4' + micrometerTracingVersion = '1.0.2' mockitoVersion = '4.8.1' rabbitmqStreamVersion = '0.8.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.16.0' reactorVersion = '2022.0.2' snappyVersion = '1.1.8.4' - springDataVersion = '2022.0.1' - springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.4' + springDataVersion = '2022.0.2' + springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.5' springRetryVersion = '2.0.0' testcontainersVersion = '1.17.6' zstdJniVersion = '1.5.0-2' diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/Base64UrlNamingStrategy.java b/spring-amqp/src/main/java/org/springframework/amqp/core/Base64UrlNamingStrategy.java index 1572600bfc..b6c6fd5a59 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/Base64UrlNamingStrategy.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/Base64UrlNamingStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2023 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. @@ -17,10 +17,10 @@ package org.springframework.amqp.core; import java.nio.ByteBuffer; +import java.util.Base64; import java.util.UUID; import org.springframework.util.Assert; -import org.springframework.util.Base64Utils; /** * Generates names with the form {@code } where 'prefix' is @@ -66,7 +66,7 @@ public String generateName() { bb.putLong(uuid.getMostSignificantBits()) .putLong(uuid.getLeastSignificantBits()); // Convert to base64 and remove trailing = - return this.prefix + Base64Utils.encodeToUrlSafeString(bb.array()) + return this.prefix + Base64.getUrlEncoder().encodeToString(bb.array()) .replaceAll("=", ""); } diff --git a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java index 544b9c480b..1a0e1147f3 100644 --- a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java +++ b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -29,6 +29,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -39,7 +40,6 @@ import org.apache.commons.logging.LogFactory; import org.springframework.http.HttpStatus; -import org.springframework.util.Base64Utils; import org.springframework.util.StringUtils; import org.springframework.web.util.UriUtils; @@ -447,7 +447,7 @@ public String generateId() { ByteBuffer bb = ByteBuffer.wrap(new byte[SIXTEEN]); bb.putLong(uuid.getMostSignificantBits()) .putLong(uuid.getLeastSignificantBits()); - return "SpringBrokerRunning." + Base64Utils.encodeToUrlSafeString(bb.array()).replaceAll("=", ""); + return "SpringBrokerRunning." + Base64.getUrlEncoder().encodeToString(bb.array()).replaceAll("=", ""); } private boolean isDefaultQueue(String queue) { From c1575df0941d8acb4688fe0f6b56bbef2b4190f3 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 21 Feb 2023 10:08:25 -0500 Subject: [PATCH 220/737] Upgrade Reactor Version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 6f0b17e2ca..8690f104ce 100644 --- a/build.gradle +++ b/build.gradle @@ -65,7 +65,7 @@ ext { mockitoVersion = '4.8.1' rabbitmqStreamVersion = '0.8.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.16.0' - reactorVersion = '2022.0.2' + reactorVersion = '2022.0.3' snappyVersion = '1.1.8.4' springDataVersion = '2022.0.2' springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.5' From 81dd39db6238aff6aa5c1f71add89209d88e97e2 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Tue, 21 Feb 2023 15:58:02 +0000 Subject: [PATCH 221/737] [artifactory-release] Release version 3.0.2 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 63baf06f92..95abdcb4ce 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.2-SNAPSHOT +version=3.0.2 org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true From b27a18fda4817d548ddca21995abc8832c0eb5c4 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Tue, 21 Feb 2023 15:58:04 +0000 Subject: [PATCH 222/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 95abdcb4ce..4de05ffee5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.2 +version=3.0.3-SNAPSHOT org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true From a0398ac4f4fe0e06b0bf62c735f6e8c6b54fc66a Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 14 Mar 2023 15:33:49 -0400 Subject: [PATCH 223/737] GH-2425: Fix NPE in ACFactory.shutdownCompleted Resolves https://github.com/spring-projects/spring-amqp/issues/2425 Assume connection problem when no cause. **cherry-pick to 2.4.x** --- .../connection/AbstractConnectionFactory.java | 8 +++-- .../AbstractConnectionFactoryTests.java | 2 +- .../CachingConnectionFactoryTests.java | 35 +++++++++++++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java index e6ebec087f..0735a691e7 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java @@ -55,6 +55,7 @@ import com.rabbitmq.client.Address; import com.rabbitmq.client.AddressResolver; import com.rabbitmq.client.BlockedListener; +import com.rabbitmq.client.Method; import com.rabbitmq.client.Recoverable; import com.rabbitmq.client.RecoveryListener; import com.rabbitmq.client.ShutdownListener; @@ -660,7 +661,11 @@ protected final String getDefaultHostName() { @Override public void shutdownCompleted(ShutdownSignalException cause) { - int protocolClassId = cause.getReason().protocolClassId(); + Method reason = cause.getReason(); + int protocolClassId = RabbitUtils.CONNECTION_PROTOCOL_CLASS_ID_10; + if (reason != null) { + protocolClassId = reason.protocolClassId(); + } if (protocolClassId == RabbitUtils.CHANNEL_PROTOCOL_CLASS_ID_20) { this.closeExceptionLogger.log(this.logger, "Shutdown Signal", cause); getChannelListener().onShutDown(cause); @@ -668,7 +673,6 @@ public void shutdownCompleted(ShutdownSignalException cause) { else if (protocolClassId == RabbitUtils.CONNECTION_PROTOCOL_CLASS_ID_10) { getConnectionListener().onShutDown(cause); } - } @Override diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactoryTests.java index cab3e670f9..60e2591b72 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2022 the original author or authors. + * Copyright 2010-2023 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. diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java index 5d8165ac88..530e93f324 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java @@ -1932,4 +1932,39 @@ void testResolver() throws Exception { verify(mockConnectionFactory).newConnection(any(ExecutorService.class), eq(resolver), anyString()); } + @Test + void nullShutdownCause() { + com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class); + AbstractConnectionFactory cf = createConnectionFactory(mockConnectionFactory); + AtomicBoolean connShutDown = new AtomicBoolean(); + cf.addConnectionListener(new ConnectionListener() { + + @Override + public void onCreate(Connection connection) { + } + + @Override + public void onShutDown(ShutdownSignalException signal) { + connShutDown.set(true); + } + + }); + AtomicBoolean chanShutDown = new AtomicBoolean(); + cf.addChannelListener(new ChannelListener() { + + @Override + public void onCreate(Channel channel, boolean transactional) { + } + + @Override + public void onShutDown(ShutdownSignalException signal) { + chanShutDown.set(true); + } + + }); + cf.shutdownCompleted(new ShutdownSignalException(false, false, null, chanShutDown)); + assertThat(connShutDown.get()).isTrue(); + assertThat(chanShutDown.get()).isFalse(); + } + } From eb2039fbb702b0e9bd5c2b813f12ef8ea2af1f7a Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Sun, 19 Mar 2023 21:37:16 -0400 Subject: [PATCH 224/737] Switch to Spring Framwork Snapshot --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 8690f104ce..7cc3335e4b 100644 --- a/build.gradle +++ b/build.gradle @@ -68,7 +68,7 @@ ext { reactorVersion = '2022.0.3' snappyVersion = '1.1.8.4' springDataVersion = '2022.0.2' - springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.5' + springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.7-SNAPSHOT' springRetryVersion = '2.0.0' testcontainersVersion = '1.17.6' zstdJniVersion = '1.5.0-2' From 7ebed9ccacc48d824f28cb9811f2a08b9775ed1d Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 21 Mar 2023 11:06:56 -0400 Subject: [PATCH 225/737] Upgrade Micrometer, Reactor, Spring Versions (#2433) Framework, Data, Retry --- build.gradle | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index 7cc3335e4b..eb9e14efe8 100644 --- a/build.gradle +++ b/build.gradle @@ -60,16 +60,16 @@ ext { logbackVersion = '1.4.4' lz4Version = '1.8.0' micrometerDocsVersion = '1.0.1' - micrometerVersion = '1.10.4' - micrometerTracingVersion = '1.0.2' + micrometerVersion = '1.10.5' + micrometerTracingVersion = '1.0.3' mockitoVersion = '4.8.1' rabbitmqStreamVersion = '0.8.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.16.0' - reactorVersion = '2022.0.3' + reactorVersion = '2022.0.5' snappyVersion = '1.1.8.4' - springDataVersion = '2022.0.2' - springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.7-SNAPSHOT' - springRetryVersion = '2.0.0' + springDataVersion = '2022.0.4' + springRetryVersion = '2.0.1' + springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.7' testcontainersVersion = '1.17.6' zstdJniVersion = '1.5.0-2' From a2da0d064fdd9e7358dcb5374bc9d108a8381e80 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Tue, 21 Mar 2023 15:26:43 +0000 Subject: [PATCH 226/737] [artifactory-release] Release version 3.0.3 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 4de05ffee5..775df24af8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.3-SNAPSHOT +version=3.0.3 org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true From ba9f8a176311073df41238260757fee79692fb92 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Tue, 21 Mar 2023 15:26:45 +0000 Subject: [PATCH 227/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 775df24af8..6ce439d014 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.3 +version=3.0.4-SNAPSHOT org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true From a5d8e36a4937d8146cfb08a9ff2afe4f211b296f Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 22 Mar 2023 13:16:21 -0400 Subject: [PATCH 228/737] GH-2432: Fix Redeclaration of Declarables Resolves https://github.com/spring-projects/spring-amqp/issues/2432 When a container starts; it looks to see if the context contains a queue bean for any of its queues and requests the admin to redeclare the infrastructure. However, if the queue is declared within a `Declarables`, the check is not performed and therefore the queue is not declared. Add a check for `Declarables` beans. **cherry-pick to 2.4.x** --- .../AbstractMessageListenerContainer.java | 13 +- .../listener/QueueDeclarationTests.java | 124 ++++++++++++++++++ 2 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/QueueDeclarationTests.java diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index c75c80740f..0426a6d35a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -21,9 +21,9 @@ import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Properties; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; @@ -42,6 +42,7 @@ import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.AmqpAdmin; import org.springframework.amqp.core.BatchMessageListener; +import org.springframework.amqp.core.Declarables; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.core.MessagePostProcessor; @@ -1977,9 +1978,11 @@ private void attemptDeclarations(AmqpAdmin admin) { ApplicationContext context = this.getApplicationContext(); if (context != null) { Set queueNames = getQueueNamesAsSet(); - Map queueBeans = context.getBeansOfType(Queue.class); - for (Entry entry : queueBeans.entrySet()) { - Queue queue = entry.getValue(); + Collection queueBeans = new LinkedHashSet<>( + context.getBeansOfType(Queue.class, false, false).values()); + Map declarables = context.getBeansOfType(Declarables.class, false, false); + declarables.values().forEach(dec -> queueBeans.addAll(dec.getDeclarablesByType(Queue.class))); + for (Queue queue : queueBeans) { if (isMismatchedQueuesFatal() || (queueNames.contains(queue.getName()) && admin.getQueueProperties(queue.getName()) == null)) { if (logger.isDebugEnabled()) { diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/QueueDeclarationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/QueueDeclarationTests.java new file mode 100644 index 0000000000..2c99f8a8b0 --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/QueueDeclarationTests.java @@ -0,0 +1,124 @@ +/* + * Copyright 2023 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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.amqp.core.AmqpAdmin; +import org.springframework.amqp.core.Declarables; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.connection.ChannelProxy; +import org.springframework.amqp.rabbit.connection.Connection; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.context.ApplicationContext; + +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Consumer; +import com.rabbitmq.client.impl.recovery.AutorecoveringChannel; + +/** + * @author Gary Russell + * @since 2.4.12 + * + */ +public class QueueDeclarationTests { + + @Test + void redeclareWhenQueue() throws IOException, InterruptedException { + AmqpAdmin admin = mock(AmqpAdmin.class); + ApplicationContext context = mock(ApplicationContext.class); + final CountDownLatch latch = new CountDownLatch(1); + SimpleMessageListenerContainer container = createContainer(admin, latch); + given(context.getBeansOfType(Queue.class, false, false)).willReturn(Map.of("foo", new Queue("test"))); + given(context.getBeansOfType(Declarables.class, false, false)).willReturn(new HashMap<>()); + container.setApplicationContext(context); + container.start(); + + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + verify(admin).initialize(); + container.stop(); + } + + @Test + void redeclareWhenDeclarables() throws IOException, InterruptedException { + AmqpAdmin admin = mock(AmqpAdmin.class); + ApplicationContext context = mock(ApplicationContext.class); + final CountDownLatch latch = new CountDownLatch(1); + SimpleMessageListenerContainer container = createContainer(admin, latch); + given(context.getBeansOfType(Queue.class, false, false)).willReturn(new HashMap<>()); + given(context.getBeansOfType(Declarables.class, false, false)) + .willReturn(Map.of("foo", new Declarables(List.of(new Queue("test"))))); + container.setApplicationContext(context); + container.start(); + + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + verify(admin).initialize(); + container.stop(); + } + + private SimpleMessageListenerContainer createContainer(AmqpAdmin admin, final CountDownLatch latch) + throws IOException { + ConnectionFactory connectionFactory = mock(ConnectionFactory.class); + Connection connection = mock(Connection.class); + ChannelProxy channel = mock(ChannelProxy.class); + Channel rabbitChannel = mock(AutorecoveringChannel.class); + given(channel.getTargetChannel()).willReturn(rabbitChannel); + + given(connectionFactory.createConnection()).willReturn(connection); + given(connection.createChannel(anyBoolean())).willReturn(channel); + final AtomicBoolean isOpen = new AtomicBoolean(true); + willAnswer(i -> isOpen.get()).given(channel).isOpen(); + given(channel.queueDeclarePassive(Mockito.anyString())) + .willAnswer(invocation -> mock(AMQP.Queue.DeclareOk.class)); + given(channel.basicConsume(anyString(), anyBoolean(), anyString(), anyBoolean(), anyBoolean(), + anyMap(), any(Consumer.class))).willReturn("consumerTag"); + + willAnswer(i -> { + latch.countDown(); + return null; + }).given(channel).basicQos(anyInt(), anyBoolean()); + + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); + container.setQueueNames("test"); + container.setPrefetchCount(2); + container.setAmqpAdmin(admin); + container.afterPropertiesSet(); + return container; + } + +} From 6d91c90fe4ccb331ad7e2ca48ca5e96b36351f4f Mon Sep 17 00:00:00 2001 From: EldarErel <52938671+EldarErel@users.noreply.github.com> Date: Wed, 22 Mar 2023 19:34:48 +0200 Subject: [PATCH 229/737] fix: #2428 - recovery manual declarables without application context (#2429) * fix: recovery manual declaration without application context * test: recovery manual declaration without application context --- .../amqp/rabbit/core/RabbitAdmin.java | 11 ++++- .../amqp/rabbit/core/RabbitAdminTests.java | 46 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java index 4cfb540d9e..3aa2684720 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java @@ -638,6 +638,8 @@ public void afterPropertiesSet() { @Override // NOSONAR complexity public void initialize() { + redeclareManualDeclarables(); + if (this.applicationContext == null) { this.logger.debug("no ApplicationContext has been set, cannot auto-declare Exchanges, Queues, and Bindings"); return; @@ -690,6 +692,14 @@ public void initialize() { declareBindings(channel, bindings.toArray(new Binding[bindings.size()])); return null; }); + this.logger.debug("Declarations finished"); + + } + + /** + * Process manual declarables. + */ + private void redeclareManualDeclarables() { if (this.manualDeclarables.size() > 0) { synchronized (this.manualDeclarables) { this.logger.debug("Redeclaring manually declared Declarables"); @@ -706,7 +716,6 @@ else if (dec instanceof Exchange exch) { } } } - this.logger.debug("Declarations finished"); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java index 9c0f2b8ff3..b36cc3a04b 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java @@ -445,6 +445,52 @@ void manualDeclarations() { cf.destroy(); } + @Test + void manualDeclarationsWithoutApplicationContext() { + CachingConnectionFactory cf = new CachingConnectionFactory( + RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); + RabbitAdmin admin = new RabbitAdmin(cf); + admin.setRedeclareManualDeclarations(true); + Map declarables = TestUtils.getPropertyValue(admin, "manualDeclarables", Map.class); + assertThat(declarables).hasSize(0); + RabbitTemplate template = new RabbitTemplate(cf); + // manual declarations + admin.declareQueue(new Queue("test1", false, true, true)); + admin.declareQueue(new Queue("test2", false, true, true)); + admin.declareExchange(new DirectExchange("ex1", false, true)); + admin.declareBinding(new Binding("test1", DestinationType.QUEUE, "ex1", "test", null)); + admin.deleteQueue("test2"); + template.execute(chan -> chan.queueDelete("test1")); + cf.resetConnection(); + admin.initialize(); + assertThat(admin.getQueueProperties("test1")).isNotNull(); + assertThat(admin.getQueueProperties("test2")).isNull(); + assertThat(declarables).hasSize(3); + // verify the exchange and binding were recovered too + template.convertAndSend("ex1", "test", "foo"); + Object received = template.receiveAndConvert("test1", 5000); + assertThat(received).isEqualTo("foo"); + admin.resetAllManualDeclarations(); + assertThat(declarables).hasSize(0); + cf.resetConnection(); + admin.initialize(); + assertThat(admin.getQueueProperties("test1")).isNull(); + admin.declareQueue(new Queue("test1", false, true, true)); + admin.declareExchange(new DirectExchange("ex1", false, true)); + admin.declareBinding(new Binding("test1", DestinationType.QUEUE, "ex1", "test", null)); + admin.declareExchange(new DirectExchange("ex2", false, true)); + admin.declareBinding(new Binding("test1", DestinationType.QUEUE, "ex2", "test", null)); + admin.declareBinding(new Binding("ex1", DestinationType.EXCHANGE, "ex2", "ex1", null)); + assertThat(declarables).hasSize(6); + admin.deleteExchange("ex2"); + assertThat(declarables).hasSize(3); + admin.deleteQueue("test1"); + assertThat(declarables).hasSize(1); + admin.deleteExchange("ex1"); + assertThat(declarables).hasSize(0); + cf.destroy(); + } + @Configuration public static class Config { From 9e2eefa885c7db1c32c49356ee50320a478e3a2a Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 23 Mar 2023 13:47:32 -0400 Subject: [PATCH 230/737] GH-2439: Fix Bindings with Broker Gen Queue Names Resolves https://github.com/spring-projects/spring-amqp/issues/2439 To configure a broker-named queue, the `Queue` name is set to an empty String; the name is later populated when the queue is declared. However, if such a queue is used in a `BindingBuilder`, the builder accesses the name before it is populated. When such a queue is used in a binding, retain a copy of the queue object and retrieve its actual name when the binding is declared. Add a unit test for all binding types; also tested with the following, with both queues being bound properly to the exchange. ```java @Bean Queue q1() { return QueueBuilder.nonDurable("").build(); } @Bean Queue q2() { return QueueBuilder.nonDurable("").build(); } @Bean Binding b1(Queue q1, DirectExchange ex) { return BindingBuilder.bind(q1).to(ex).with("foo"); } @Bean Binding b2(Queue q2, DirectExchange ex) { return BindingBuilder.bind(q2).to(ex).with("bar"); } @Bean DirectExchange ex() { return new DirectExchange("ex"); } ``` **cherry-pick to 2.4.x** --- .../springframework/amqp/core/Binding.java | 28 +++- .../amqp/core/BindingBuilder.java | 53 ++++--- .../BindingBuilderWithLazyQueueNameTests.java | 129 ++++++++++++++++++ 3 files changed, 192 insertions(+), 18 deletions(-) create mode 100644 spring-amqp/src/test/java/org/springframework/amqp/core/BindingBuilderWithLazyQueueNameTests.java diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/Binding.java b/spring-amqp/src/main/java/org/springframework/amqp/core/Binding.java index abaf75e432..a5ee74273f 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/Binding.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/Binding.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -19,6 +19,7 @@ import java.util.Map; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * Simple container collecting information to describe a binding. Takes String destination and exchange names as @@ -50,18 +51,33 @@ public enum DestinationType { EXCHANGE; } + @Nullable private final String destination; private final String exchange; + @Nullable private final String routingKey; private final DestinationType destinationType; + @Nullable + private final Queue lazyQueue; + public Binding(String destination, DestinationType destinationType, String exchange, String routingKey, @Nullable Map arguments) { + this(null, destination, destinationType, exchange, routingKey, arguments); + } + + public Binding(@Nullable Queue lazyQueue, @Nullable String destination, DestinationType destinationType, + String exchange, @Nullable String routingKey, @Nullable Map arguments) { + super(arguments); + Assert.isTrue(lazyQueue == null || destinationType.equals(DestinationType.QUEUE), + "'lazyQueue' must be null for destination type " + destinationType); + Assert.isTrue(lazyQueue != null || destination != null, "`destination` cannot be null"); + this.lazyQueue = lazyQueue; this.destination = destination; this.destinationType = destinationType; this.exchange = exchange; @@ -69,7 +85,12 @@ public Binding(String destination, DestinationType destinationType, String excha } public String getDestination() { - return this.destination; + if (this.lazyQueue != null) { + return this.lazyQueue.getActualName(); + } + else { + return this.destination; + } } public DestinationType getDestinationType() { @@ -81,6 +102,9 @@ public String getExchange() { } public String getRoutingKey() { + if (this.routingKey == null && this.lazyQueue != null) { + return this.lazyQueue.getActualName(); + } return this.routingKey; } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/BindingBuilder.java b/spring-amqp/src/main/java/org/springframework/amqp/core/BindingBuilder.java index b1105d2550..642481750d 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/BindingBuilder.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/BindingBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -37,7 +37,12 @@ private BindingBuilder() { } public static DestinationConfigurer bind(Queue queue) { - return new DestinationConfigurer(queue.getName(), DestinationType.QUEUE); + if ("".equals(queue.getName())) { + return new DestinationConfigurer(queue, DestinationType.QUEUE); + } + else { + return new DestinationConfigurer(queue.getName(), DestinationType.QUEUE); + } } public static DestinationConfigurer bind(Exchange exchange) { @@ -61,13 +66,22 @@ public static final class DestinationConfigurer { protected final DestinationType type; // NOSONAR + protected final Queue queue; // NOSONAR + DestinationConfigurer(String name, DestinationType type) { + this.queue = null; this.name = name; this.type = type; } + DestinationConfigurer(Queue queue, DestinationType type) { + this.queue = queue; + this.name = null; + this.type = type; + } + public Binding to(FanoutExchange exchange) { - return new Binding(this.name, this.type, exchange.getName(), "", new HashMap()); + return new Binding(this.queue, this.name, this.type, exchange.getName(), "", new HashMap()); } public HeadersExchangeMapConfigurer to(HeadersExchange exchange) { @@ -134,7 +148,8 @@ public final class HeadersExchangeSingleValueBindingCreator { } public Binding exists() { - return new Binding(HeadersExchangeMapConfigurer.this.destination.name, + return new Binding(HeadersExchangeMapConfigurer.this.destination.queue, + HeadersExchangeMapConfigurer.this.destination.name, HeadersExchangeMapConfigurer.this.destination.type, HeadersExchangeMapConfigurer.this.exchange.getName(), "", createMapForKeys(this.key)); } @@ -142,7 +157,8 @@ public Binding exists() { public Binding matches(Object value) { Map map = new HashMap(); map.put(this.key, value); - return new Binding(HeadersExchangeMapConfigurer.this.destination.name, + return new Binding(HeadersExchangeMapConfigurer.this.destination.queue, + HeadersExchangeMapConfigurer.this.destination.name, HeadersExchangeMapConfigurer.this.destination.type, HeadersExchangeMapConfigurer.this.exchange.getName(), "", map); } @@ -162,7 +178,8 @@ public final class HeadersExchangeKeysBindingCreator { } public Binding exist() { - return new Binding(HeadersExchangeMapConfigurer.this.destination.name, + return new Binding(HeadersExchangeMapConfigurer.this.destination.queue, + HeadersExchangeMapConfigurer.this.destination.name, HeadersExchangeMapConfigurer.this.destination.type, HeadersExchangeMapConfigurer.this.exchange.getName(), "", this.headerMap); } @@ -182,7 +199,8 @@ public final class HeadersExchangeMapBindingCreator { } public Binding match() { - return new Binding(HeadersExchangeMapConfigurer.this.destination.name, + return new Binding(HeadersExchangeMapConfigurer.this.destination.queue, + HeadersExchangeMapConfigurer.this.destination.name, HeadersExchangeMapConfigurer.this.destination.type, HeadersExchangeMapConfigurer.this.exchange.getName(), "", this.headerMap); } @@ -211,13 +229,13 @@ public static final class TopicExchangeRoutingKeyConfigurer extends AbstractRout } public Binding with(String routingKey) { - return new Binding(destination.name, destination.type, exchange, routingKey, + return new Binding(destination.queue, destination.name, destination.type, exchange, routingKey, Collections.emptyMap()); } public Binding with(Enum routingKeyEnum) { - return new Binding(destination.name, destination.type, exchange, routingKeyEnum.toString(), - Collections.emptyMap()); + return new Binding(destination.queue, destination.name, destination.type, exchange, + routingKeyEnum.toString(), Collections.emptyMap()); } } @@ -255,12 +273,14 @@ public GenericArgumentsConfigurer(GenericExchangeRoutingKeyConfigurer configurer } public Binding and(Map map) { - return new Binding(this.configurer.destination.name, this.configurer.destination.type, this.configurer.exchange, + return new Binding(this.configurer.destination.queue, + this.configurer.destination.name, this.configurer.destination.type, this.configurer.exchange, this.routingKey, map); } public Binding noargs() { - return new Binding(this.configurer.destination.name, this.configurer.destination.type, this.configurer.exchange, + return new Binding(this.configurer.destination.queue, + this.configurer.destination.name, this.configurer.destination.type, this.configurer.exchange, this.routingKey, Collections.emptyMap()); } @@ -276,19 +296,20 @@ public static final class DirectExchangeRoutingKeyConfigurer extends AbstractRou } public Binding with(String routingKey) { - return new Binding(destination.name, destination.type, exchange, routingKey, + return new Binding(destination.queue, destination.name, destination.type, exchange, routingKey, Collections.emptyMap()); } public Binding with(Enum routingKeyEnum) { - return new Binding(destination.name, destination.type, exchange, routingKeyEnum.toString(), - Collections.emptyMap()); + return new Binding(destination.queue, destination.name, destination.type, exchange, + routingKeyEnum.toString(), Collections.emptyMap()); } public Binding withQueueName() { - return new Binding(destination.name, destination.type, exchange, destination.name, + return new Binding(destination.queue, destination.name, destination.type, exchange, destination.name, Collections.emptyMap()); } + } } diff --git a/spring-amqp/src/test/java/org/springframework/amqp/core/BindingBuilderWithLazyQueueNameTests.java b/spring-amqp/src/test/java/org/springframework/amqp/core/BindingBuilderWithLazyQueueNameTests.java new file mode 100644 index 0000000000..5fe956b5a8 --- /dev/null +++ b/spring-amqp/src/test/java/org/springframework/amqp/core/BindingBuilderWithLazyQueueNameTests.java @@ -0,0 +1,129 @@ +/* + * Copyright 2023 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.core; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Collections; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Copy of {@link BindingBuilderTests} but using a queue with a lazy name. + * + * @author Mark Fisher + * @author Artem Yakshin + * @author Gary Russell + */ +public class BindingBuilderWithLazyQueueNameTests { + + private static Queue queue; + + @BeforeAll + public static void setUp() { + queue = new Queue(""); + queue.setActualName("actual"); + } + + @Test + public void fanoutBinding() { + FanoutExchange fanoutExchange = new FanoutExchange("f"); + Binding binding = BindingBuilder.bind(queue).to(fanoutExchange); + assertThat(binding).isNotNull(); + assertThat(binding.getExchange()).isEqualTo(fanoutExchange.getName()); + assertThat(binding.getRoutingKey()).isEqualTo(""); + assertThat(binding.getDestinationType()).isEqualTo(Binding.DestinationType.QUEUE); + assertThat(binding.getDestination()).isEqualTo(queue.getActualName()); + } + + @Test + public void directBinding() { + DirectExchange directExchange = new DirectExchange("d"); + String routingKey = "r"; + Binding binding = BindingBuilder.bind(queue).to(directExchange).with(routingKey); + assertThat(binding).isNotNull(); + assertThat(binding.getExchange()).isEqualTo(directExchange.getName()); + assertThat(binding.getDestinationType()).isEqualTo(Binding.DestinationType.QUEUE); + assertThat(binding.getDestination()).isEqualTo(queue.getActualName()); + assertThat(binding.getRoutingKey()).isEqualTo(routingKey); + } + + @Test + public void directBindingWithQueueName() { + DirectExchange directExchange = new DirectExchange("d"); + Binding binding = BindingBuilder.bind(queue).to(directExchange).withQueueName(); + assertThat(binding).isNotNull(); + assertThat(binding.getExchange()).isEqualTo(directExchange.getName()); + assertThat(binding.getDestinationType()).isEqualTo(Binding.DestinationType.QUEUE); + assertThat(binding.getDestination()).isEqualTo(queue.getActualName()); + assertThat(binding.getRoutingKey()).isEqualTo(queue.getActualName()); + } + + @Test + public void topicBinding() { + TopicExchange topicExchange = new TopicExchange("t"); + String routingKey = "r"; + Binding binding = BindingBuilder.bind(queue).to(topicExchange).with(routingKey); + assertThat(binding).isNotNull(); + assertThat(binding.getExchange()).isEqualTo(topicExchange.getName()); + assertThat(binding.getDestinationType()).isEqualTo(Binding.DestinationType.QUEUE); + assertThat(binding.getDestination()).isEqualTo(queue.getActualName()); + assertThat(binding.getRoutingKey()).isEqualTo(routingKey); + } + + @Test + public void headerBinding() { + HeadersExchange headersExchange = new HeadersExchange("h"); + String headerKey = "headerKey"; + Binding binding = BindingBuilder.bind(queue).to(headersExchange).where(headerKey).exists(); + assertThat(binding).isNotNull(); + assertThat(binding.getExchange()).isEqualTo(headersExchange.getName()); + assertThat(binding.getDestinationType()).isEqualTo(Binding.DestinationType.QUEUE); + assertThat(binding.getDestination()).isEqualTo(queue.getActualName()); + assertThat(binding.getRoutingKey()).isEqualTo(""); + } + + @Test + public void customBinding() { + class CustomExchange extends AbstractExchange { + CustomExchange(String name) { + super(name); + } + + @Override + public String getType() { + return "x-custom"; + } + } + Object argumentObject = new Object(); + CustomExchange customExchange = new CustomExchange("c"); + String routingKey = "r"; + Binding binding = BindingBuilder.// + bind(queue).// + to(customExchange).// + with(routingKey).// + and(Collections.singletonMap("k", argumentObject)); + assertThat(binding).isNotNull(); + assertThat(binding.getArguments().get("k")).isEqualTo(argumentObject); + assertThat(binding.getExchange()).isEqualTo(customExchange.getName()); + assertThat(binding.getDestinationType()).isEqualTo(Binding.DestinationType.QUEUE); + assertThat(binding.getDestination()).isEqualTo(queue.getActualName()); + assertThat(binding.getRoutingKey()).isEqualTo(routingKey); + } + +} From 976b3df760ef0458464cde5019ae5f523386298a Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Fri, 24 Mar 2023 09:30:28 -0400 Subject: [PATCH 231/737] GH-2437: Fix Fatal When No Matching RabbitHandler Resolves https://github.com/spring-projects/spring-amqp/issues/2437 Failure to find a `@RabbitHandler` method for the payload must be treated as a fatal exception to avoid an infinite loop. Also, fix CREH cause traversal to stop when **any** fatal exception is found. Previously, traversal only stopped for the original subset of exceptions. **cherry-pick to 2.4.x** --- .../ConditionalRejectingErrorHandler.java | 9 ++- .../adapter/DelegatingInvocableHandler.java | 7 +- .../DelegatingInvocableHandlerTests.java | 76 +++++++++++++++++++ 3 files changed, 86 insertions(+), 6 deletions(-) create mode 100644 spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandlerTests.java diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ConditionalRejectingErrorHandler.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ConditionalRejectingErrorHandler.java index 4ddb6c1a5c..acd7cbedb8 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ConditionalRejectingErrorHandler.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ConditionalRejectingErrorHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2023 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. @@ -16,6 +16,7 @@ package org.springframework.amqp.rabbit.listener; +import java.lang.reflect.UndeclaredThrowableException; import java.util.List; import java.util.Map; @@ -200,9 +201,9 @@ public static class DefaultExceptionStrategy implements FatalExceptionStrategy { @Override public boolean isFatal(Throwable t) { Throwable cause = t.getCause(); - while (cause instanceof MessagingException - && !(cause instanceof org.springframework.messaging.converter.MessageConversionException) - && !(cause instanceof MethodArgumentResolutionException)) { + while ((cause instanceof MessagingException || cause instanceof UndeclaredThrowableException) + && !isCauseFatal(cause)) { + cause = cause.getCause(); } if (t instanceof ListenerExecutionFailedException lefe && isCauseFatal(cause)) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandler.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandler.java index 12fbf966c7..4316d125af 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandler.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2022 the original author or authors. + * Copyright 2015-2023 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. @@ -46,6 +46,7 @@ import org.springframework.messaging.handler.annotation.support.PayloadMethodArgumentResolver; import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; import org.springframework.validation.Validator; @@ -204,7 +205,9 @@ protected InvocableHandlerMethod getHandlerForPayload(Class pa if (handler == null) { handler = findHandlerForPayload(payloadClass); if (handler == null) { - throw new AmqpException("No method found for " + payloadClass); + ReflectionUtils.rethrowRuntimeException( + new NoSuchMethodException("No listener method found in " + this.bean.getClass().getName() + + " for " + payloadClass)); } this.cachedHandlers.putIfAbsent(payloadClass, handler); //NOSONAR setupReplyTo(handler); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandlerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandlerTests.java new file mode 100644 index 0000000000..f3ef0314cd --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandlerTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2023 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.assertThatExceptionOfType; +import static org.mockito.Mockito.mock; + +import java.lang.reflect.Method; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.config.BeanExpressionContext; +import org.springframework.beans.factory.config.BeanExpressionResolver; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.messaging.converter.GenericMessageConverter; +import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory; +import org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory; +import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; + +/** + * @author Gary Russell + * @since 2.4.12 + * + */ +public class DelegatingInvocableHandlerTests { + + @Test + void multiNoMatch() throws Exception { + List methods = new ArrayList<>(); + Object bean = new Multi(); + Method method = Multi.class.getDeclaredMethod("listen", Integer.class); + methods.add(messageHandlerFactory().createInvocableHandlerMethod(bean, method)); + BeanExpressionResolver resolver = mock(BeanExpressionResolver.class); + BeanExpressionContext context = mock(BeanExpressionContext.class); + DelegatingInvocableHandler handler = new DelegatingInvocableHandler(methods, bean, resolver, context); + assertThatExceptionOfType(UndeclaredThrowableException.class).isThrownBy(() -> + handler.getHandlerForPayload(Long.class)) + .withCauseExactlyInstanceOf(NoSuchMethodException.class) + .withStackTraceContaining("No listener method found in"); + } + + private MessageHandlerMethodFactory messageHandlerFactory() { + DefaultMessageHandlerMethodFactory defaultFactory = new DefaultMessageHandlerMethodFactory(); + DefaultFormattingConversionService cs = new DefaultFormattingConversionService(); + defaultFactory.setConversionService(cs); + GenericMessageConverter messageConverter = new GenericMessageConverter(cs); + defaultFactory.setMessageConverter(messageConverter); + defaultFactory.afterPropertiesSet(); + return defaultFactory; + } + + public static class Multi { + + void listen(Integer in) { + } + + } + +} From b117372ea84a8942d22ff2d60a3a4f77158fcb4f Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 10 Apr 2023 11:15:17 -0400 Subject: [PATCH 232/737] GH-2445: Doc Stream Provisioning and Dependency Resolves https://github.com/spring-projects/spring-amqp/issues/2445 --- src/reference/asciidoc/stream.adoc | 40 ++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/reference/asciidoc/stream.adoc b/src/reference/asciidoc/stream.adoc index dda312392a..ca3e07a980 100644 --- a/src/reference/asciidoc/stream.adoc +++ b/src/reference/asciidoc/stream.adoc @@ -6,6 +6,31 @@ Version 2.4 introduces initial support for the https://github.com/rabbitmq/rabbi * `RabbitStreamTemplate` * `StreamListenerContainer` +Add the `spring-rabbit-stream` dependency to your project: + +.maven +==== +[source,xml,subs="+attributes"] +---- + + org.springframework.amqp + spring-rabbit-stream + {project-version} + +---- +==== + +.gradle +==== +[source,groovy,subs="+attributes"] +---- +compile 'org.springframework.amqp:spring-rabbit-stream:{project-version}' +---- +==== + +Provision the queues as normal, using a `RabbitAdmin` bean, using the `QueueBuilder.stream()` method to designate the queue type. +See <>. + ==== Sending Messages The `RabbitStreamTemplate` provides a subset of the `RabbitTemplate` (AMQP) functionality. @@ -97,6 +122,7 @@ Refer to the https://rabbitmq.github.io/rabbitmq-stream-java-client/stable/htmls When using `@RabbitListener`, configure a `StreamRabbitListenerContainerFactory`; at this time, most `@RabbitListener` properties (`concurrency`, etc) are ignored. Only `id`, `queues`, `autoStartup` and `containerFactory` are supported. In addition, `queues` can only contain one stream name. +[[stream-examples]] ==== Examples ==== @@ -136,6 +162,20 @@ void nativeMsg(Message in, Context context) { ... context.storeOffset(); } + +@Bean +Queue stream() { + return QueueBuilder.durable("test.stream.queue1") + .stream() + .build(); +} + +@Bean +Queue stream() { + return QueueBuilder.durable("test.stream.queue2") + .stream() + .build(); +} ---- ==== From d77e2d4d54a676df1ff5f707aecc5f5328ac88bb Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 11 Apr 2023 17:12:02 -0400 Subject: [PATCH 233/737] GH-2447: Publisher Confirms/Returns Doc Polishing Resolves https://github.com/spring-projects/spring-amqp/issues/2447 --- .../amqp/rabbit/connection/CorrelationData.java | 4 ++-- src/reference/asciidoc/amqp.adoc | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java index 9967ffa1f1..476d21646c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -96,7 +96,7 @@ public CompletableFuture getFuture() { /** * Get the returned message and metadata, if any. Guaranteed to be populated before - * the future is set. + * the future is completed. * @return the {@link ReturnedMessage}. * @since 2.3.3 */ diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index b8a0c2bdf7..ccb8ab665a 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -851,7 +851,7 @@ When such a channel is obtained, the client can register a `PublisherCallbackCha The `PublisherCallbackChannel` implementation contains logic to route a confirm or return to the appropriate listener. These features are explained further in the following sections. -See also `simplePublisherConfirms` in <>. +See also <> and `simplePublisherConfirms` in <>. TIP: For some more background information, see the blog post by the RabbitMQ team titled https://www.rabbitmq.com/blog/2011/02/10/introducing-publisher-confirms/[Introducing Publisher Confirms]. @@ -1278,6 +1278,8 @@ The following example shows how to configure a `CorrelationData` instance: CorrelationData cd1 = new CorrelationData(); this.templateWithConfirmsEnabled.convertAndSend("exchange", queue.getName(), "foo", cd1); assertTrue(cd1.getFuture().get(10, TimeUnit.SECONDS).isAck()); +ReturnedMessage = cd1.getReturn(); +... ---- ==== @@ -1286,8 +1288,15 @@ The `Confirm` object is a simple bean with 2 properties: `ack` and `reason` (for The reason is not populated for broker-generated `nack` instances. It is populated for `nack` instances generated by the framework (for example, closing the connection while `ack` instances are outstanding). -In addition, when both confirms and returns are enabled, the `CorrelationData` is populated with the returned message, as long as the `CorrelationData` has a unique `id`; this is always the case, by default, starting with version 2.3. -It is guaranteed that the returned message is set before the future is set with the `ack`. +In addition, when both confirms and returns are enabled, the `CorrelationData` `return` property is populated with the returned message, if it couldn't be routed to any queue. +It is guaranteed that the returned message property is set before the future is set with the `ack`. +`CorrelationData.getReturn()` returns a `ReturnMessage` with properties: + +* message (the returned message) +* replyCode +* replyText +* exchange +* routingKey See also <> for a simpler mechanism for waiting for publisher confirms. From 56b250e93dad27fa2db46c1bc9f995c5cf29b021 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 13 Apr 2023 14:36:45 -0400 Subject: [PATCH 234/737] GH-1410: AmqpTemplate Javadoc Polishing Resolves https://github.com/spring-projects/spring-amqp/issues/1410 Explain that send and receive methods return null on timeout. --- .../amqp/core/AmqpTemplate.java | 128 +++++++++--------- 1 file changed, 64 insertions(+), 64 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpTemplate.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpTemplate.java index 37ad90fbc6..3037e54096 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpTemplate.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpTemplate.java @@ -419,9 +419,9 @@ boolean receiveAndReply(String queueName, ReceiveAndReplyCallback c * and attempt to receive a response. Implementations will normally set the reply-to * header to an exclusive queue and wait up for some time limited by a timeout. * - * @param message a message to send - * @return the response if there is one - * @throws AmqpException if there is a problem + * @param message a message to send. + * @return the response; or null if the reply times out. + * @throws AmqpException if there is a problem. */ @Nullable Message sendAndReceive(Message message) throws AmqpException; @@ -431,10 +431,10 @@ boolean receiveAndReply(String queueName, ReceiveAndReplyCallback c * and attempt to receive a response. Implementations will normally set the reply-to * header to an exclusive queue and wait up for some time limited by a timeout. * - * @param routingKey the routing key - * @param message a message to send - * @return the response if there is one - * @throws AmqpException if there is a problem + * @param routingKey the routing key. + * @param message a message to send. + * @return the response; or null if the reply times out. + * @throws AmqpException if there is a problem. */ @Nullable Message sendAndReceive(String routingKey, Message message) throws AmqpException; @@ -445,11 +445,11 @@ boolean receiveAndReply(String queueName, ReceiveAndReplyCallback c * reply-to header to an exclusive queue and wait up for some time limited by a * timeout. * - * @param exchange the name of the exchange - * @param routingKey the routing key - * @param message a message to send - * @return the response if there is one - * @throws AmqpException if there is a problem + * @param exchange the name of the exchange. + * @param routingKey the routing key. + * @param message a message to send. + * @return the response; or null if the reply times out. + * @throws AmqpException if there is a problem. */ @Nullable Message sendAndReceive(String exchange, String routingKey, Message message) throws AmqpException; @@ -462,9 +462,9 @@ boolean receiveAndReply(String queueName, ReceiveAndReplyCallback c * converting that to a Java object. Implementations will normally set the reply-to * header to an exclusive queue and wait up for some time limited by a timeout. * - * @param message a message to send - * @return the response if there is one - * @throws AmqpException if there is a problem + * @param message a message to send. + * @return the response; or null if the reply times out. + * @throws AmqpException if there is a problem. */ @Nullable Object convertSendAndReceive(Object message) throws AmqpException; @@ -475,10 +475,10 @@ boolean receiveAndReply(String queueName, ReceiveAndReplyCallback c * converting that to a Java object. Implementations will normally set the reply-to * header to an exclusive queue and wait up for some time limited by a timeout. * - * @param routingKey the routing key - * @param message a message to send - * @return the response if there is one - * @throws AmqpException if there is a problem + * @param routingKey the routing key. + * @param message a message to send. + * @return the response; or null if the reply times out. + * @throws AmqpException if there is a problem. */ @Nullable Object convertSendAndReceive(String routingKey, Object message) throws AmqpException; @@ -489,11 +489,11 @@ boolean receiveAndReply(String queueName, ReceiveAndReplyCallback c * converting that to a Java object. Implementations will normally set the reply-to * header to an exclusive queue and wait up for some time limited by a timeout. * - * @param exchange the name of the exchange - * @param routingKey the routing key - * @param message a message to send - * @return the response if there is one - * @throws AmqpException if there is a problem + * @param exchange the name of the exchange. + * @param routingKey the routing key. + * @param message a message to send. + * @return the response; or null if the reply times out. + * @throws AmqpException if there is a problem. */ @Nullable Object convertSendAndReceive(String exchange, String routingKey, Object message) throws AmqpException; @@ -504,10 +504,10 @@ boolean receiveAndReply(String queueName, ReceiveAndReplyCallback c * converting that to a Java object. Implementations will normally set the reply-to * header to an exclusive queue and wait up for some time limited by a timeout. * - * @param message a message to send - * @param messagePostProcessor a processor to apply to the message before it is sent - * @return the response if there is one - * @throws AmqpException if there is a problem + * @param message a message to send. + * @param messagePostProcessor a processor to apply to the message before it is sent. + * @return the response; or null if the reply times out. + * @throws AmqpException if there is a problem. */ @Nullable Object convertSendAndReceive(Object message, MessagePostProcessor messagePostProcessor) throws AmqpException; @@ -518,11 +518,11 @@ boolean receiveAndReply(String queueName, ReceiveAndReplyCallback c * converting that to a Java object. Implementations will normally set the reply-to * header to an exclusive queue and wait up for some time limited by a timeout. * - * @param routingKey the routing key - * @param message a message to send - * @param messagePostProcessor a processor to apply to the message before it is sent - * @return the response if there is one - * @throws AmqpException if there is a problem + * @param routingKey the routing key. + * @param message a message to send. + * @param messagePostProcessor a processor to apply to the message before it is sent. + * @return the response; or null if the reply times out. + * @throws AmqpException if there is a problem. */ @Nullable Object convertSendAndReceive(String routingKey, Object message, MessagePostProcessor messagePostProcessor) @@ -534,12 +534,12 @@ Object convertSendAndReceive(String routingKey, Object message, MessagePostProce * converting that to a Java object. Implementations will normally set the reply-to * header to an exclusive queue and wait up for some time limited by a timeout. * - * @param exchange the name of the exchange - * @param routingKey the routing key - * @param message a message to send - * @param messagePostProcessor a processor to apply to the message before it is sent - * @return the response if there is one - * @throws AmqpException if there is a problem + * @param exchange the name of the exchange. + * @param routingKey the routing key. + * @param message a message to send. + * @param messagePostProcessor a processor to apply to the message before it is sent. + * @return the response; or null if the reply times out. + * @throws AmqpException if there is a problem. */ @Nullable Object convertSendAndReceive(String exchange, String routingKey, Object message, @@ -555,7 +555,7 @@ Object convertSendAndReceive(String exchange, String routingKey, Object message, * @param message a message to send. * @param responseType the type to convert the reply to. * @param the type. - * @return the response if there is one. + * @return the response; or null if the reply times out. * @throws AmqpException if there is a problem. * @since 2.0 */ @@ -569,12 +569,12 @@ T convertSendAndReceiveAsType(Object message, ParameterizedTypeReference * converting that to a Java object. Implementations will normally set the reply-to * header to an exclusive queue and wait up for some time limited by a timeout. * Requires a {@link org.springframework.amqp.support.converter.SmartMessageConverter}. - * @param routingKey the routing key - * @param message a message to send + * @param routingKey the routing key. + * @param message a message to send. * @param responseType the type to convert the reply to. * @param the type. - * @return the response if there is one - * @throws AmqpException if there is a problem + * @return the response; or null if the reply times out. + * @throws AmqpException if there is a problem. * @since 2.0 */ @Nullable @@ -587,13 +587,13 @@ T convertSendAndReceiveAsType(String routingKey, Object message, * converting that to a Java object. Implementations will normally set the reply-to * header to an exclusive queue and wait up for some time limited by a timeout. * Requires a {@link org.springframework.amqp.support.converter.SmartMessageConverter}. - * @param exchange the name of the exchange - * @param routingKey the routing key - * @param message a message to send + * @param exchange the name of the exchange. + * @param routingKey the routing key. + * @param message a message to send. * @param responseType the type to convert the reply to. * @param the type. - * @return the response if there is one - * @throws AmqpException if there is a problem + * @return the response; or null if the reply times out. + * @throws AmqpException if there is a problem. * @since 2.0 */ @Nullable @@ -606,12 +606,12 @@ T convertSendAndReceiveAsType(String exchange, String routingKey, Object mes * converting that to a Java object. Implementations will normally set the reply-to * header to an exclusive queue and wait up for some time limited by a timeout. * Requires a {@link org.springframework.amqp.support.converter.SmartMessageConverter}. - * @param message a message to send - * @param messagePostProcessor a processor to apply to the message before it is sent + * @param message a message to send. + * @param messagePostProcessor a processor to apply to the message before it is sent. * @param responseType the type to convert the reply to. * @param the type. - * @return the response if there is one - * @throws AmqpException if there is a problem + * @return the response; or null if the reply times out. + * @throws AmqpException if there is a problem. * @since 2.0 */ @Nullable @@ -624,13 +624,13 @@ T convertSendAndReceiveAsType(Object message, MessagePostProcessor messagePo * converting that to a Java object. Implementations will normally set the reply-to * header to an exclusive queue and wait up for some time limited by a timeout. * Requires a {@link org.springframework.amqp.support.converter.SmartMessageConverter}. - * @param routingKey the routing key - * @param message a message to send - * @param messagePostProcessor a processor to apply to the message before it is sent + * @param routingKey the routing key. + * @param message a message to send. + * @param messagePostProcessor a processor to apply to the message before it is sent. * @param responseType the type to convert the reply to. * @param the type. - * @return the response if there is one - * @throws AmqpException if there is a problem + * @return the response; or null if the reply times out. + * @throws AmqpException if there is a problem. * @since 2.0 */ @Nullable @@ -644,14 +644,14 @@ T convertSendAndReceiveAsType(String routingKey, Object message, * converting that to a Java object. Implementations will normally set the reply-to * header to an exclusive queue and wait up for some time limited by a timeout. * Requires a {@link org.springframework.amqp.support.converter.SmartMessageConverter}. - * @param exchange the name of the exchange - * @param routingKey the routing key - * @param message a message to send - * @param messagePostProcessor a processor to apply to the message before it is sent + * @param exchange the name of the exchange. + * @param routingKey the routing key. + * @param message a message to send. + * @param messagePostProcessor a processor to apply to the message before it is sent. * @param responseType the type to convert the reply to. * @param the type. - * @return the response if there is one - * @throws AmqpException if there is a problem + * @return the response; or null if the reply times out. + * @throws AmqpException if there is a problem. * @since 2.0 */ @Nullable From db15aec46a406acf53d4d1fa7db415c667c6ba53 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 17 Apr 2023 10:45:19 -0400 Subject: [PATCH 235/737] Upgrade Spring Framework, Data, Micrometer, Reactor, JUnit Versions (#2450) --- build.gradle | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index eb9e14efe8..f0d3686afc 100644 --- a/build.gradle +++ b/build.gradle @@ -55,21 +55,21 @@ ext { jacksonBomVersion = '2.14.2' jaywayJsonPathVersion = '2.7.0' junit4Version = '4.13.2' - junitJupiterVersion = '5.9.1' + junitJupiterVersion = '5.9.2' log4jVersion = '2.19.0' logbackVersion = '1.4.4' lz4Version = '1.8.0' micrometerDocsVersion = '1.0.1' - micrometerVersion = '1.10.5' - micrometerTracingVersion = '1.0.3' + micrometerVersion = '1.10.6' + micrometerTracingVersion = '1.0.4' mockitoVersion = '4.8.1' rabbitmqStreamVersion = '0.8.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.16.0' - reactorVersion = '2022.0.5' + reactorVersion = '2022.0.6' snappyVersion = '1.1.8.4' - springDataVersion = '2022.0.4' + springDataVersion = '2022.0.5' springRetryVersion = '2.0.1' - springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.7' + springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.8' testcontainersVersion = '1.17.6' zstdJniVersion = '1.5.0-2' From da1e2ac27d2ad7949795a80cf4faff18ff4d0641 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 17 Apr 2023 15:53:08 +0000 Subject: [PATCH 236/737] [artifactory-release] Release version 3.0.4 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 6ce439d014..7bf9fc6fd1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.4-SNAPSHOT +version=3.0.4 org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true From 5ccf51f4b3a5993fbc621fb235376a3caf4313a6 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 17 Apr 2023 15:53:10 +0000 Subject: [PATCH 237/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 7bf9fc6fd1..d12bbe7c8b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.4 +version=3.0.5-SNAPSHOT org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true From ee681bd01540e1e980c33a2c61eeb7af51ef957a Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 1 May 2023 13:00:51 -0400 Subject: [PATCH 238/737] GH-2452: Redeclare manual entities automatically (#2453) Fixes https://github.com/spring-projects/spring-amqp/issues/2452 The queue might be declared manually by an `AmqpAdmin` and its name simply can be used in the listener container. When we lose a connection, we would like to have just deleted manual anonymous queue to be redeclared. * Introduce `AmqpAdmin.getManualDeclarables()` * Check for queue name presence in the `manualDeclarables` as well from a `AbstractMessageListenerContainer.attemptDeclarations()` * Modify `DirectMessageListenerContainerIntegrationTests.testRecoverDeletedQueueGuts()` to deal with manual declaration and auto-recovery from the container **Cherry-pick to `2.4.x`** --- .../springframework/amqp/core/AmqpAdmin.java | 14 ++++++++++- .../amqp/rabbit/core/RabbitAdmin.java | 12 ++++++++- .../AbstractMessageListenerContainer.java | 15 ++++++++--- ...sageListenerContainerIntegrationTests.java | 25 +++++++++---------- 4 files changed, 47 insertions(+), 19 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpAdmin.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpAdmin.java index 62d8566d34..91156cbd4c 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpAdmin.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpAdmin.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -16,6 +16,8 @@ package org.springframework.amqp.core; +import java.util.Collections; +import java.util.Map; import java.util.Properties; import org.springframework.lang.Nullable; @@ -27,6 +29,7 @@ * @author Mark Pollack * @author Dave Syer * @author Gary Russell + * @author Artem Bilan */ public interface AmqpAdmin { @@ -126,6 +129,15 @@ public interface AmqpAdmin { @Nullable QueueInformation getQueueInfo(String queueName); + /** + * Return the manually declared AMQP objects. + * @return the manually declared AMQP objects. + * @since 2.4.13 + */ + default Map getManualDeclarables() { + return Collections.emptyMap(); + } + /** * Initialize the admin. * @since 2.1 diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java index 3aa2684720..ed1b3bf846 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -729,6 +729,16 @@ public void resetAllManualDeclarations() { this.manualDeclarables.clear(); } + /** + * Return the manually declared AMQP objects. + * @return the manually declared AMQP objects. + * @since 2.4.13 + */ + @Override + public Map getManualDeclarables() { + return Collections.unmodifiableMap(this.manualDeclarables); + } + private void processDeclarables(Collection contextExchanges, Collection contextQueues, Collection contextBindings) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index 0426a6d35a..7bf4483ddd 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -618,6 +618,7 @@ protected final String getBeanName() { return this.beanName; } + @Nullable protected final ApplicationContext getApplicationContext() { return this.applicationContext; } @@ -1975,14 +1976,20 @@ protected synchronized void redeclareElementsIfNecessary() { } private void attemptDeclarations(AmqpAdmin admin) { - ApplicationContext context = this.getApplicationContext(); + ApplicationContext context = getApplicationContext(); if (context != null) { Set queueNames = getQueueNamesAsSet(); - Collection queueBeans = new LinkedHashSet<>( + Collection queues = new LinkedHashSet<>( context.getBeansOfType(Queue.class, false, false).values()); Map declarables = context.getBeansOfType(Declarables.class, false, false); - declarables.values().forEach(dec -> queueBeans.addAll(dec.getDeclarablesByType(Queue.class))); - for (Queue queue : queueBeans) { + declarables.values().forEach(dec -> queues.addAll(dec.getDeclarablesByType(Queue.class))); + admin.getManualDeclarables() + .values() + .stream() + .filter(Queue.class::isInstance) + .map(Queue.class::cast) + .forEach(queues::add); + for (Queue queue : queues) { if (isMismatchedQueuesFatal() || (queueNames.contains(queue.getName()) && admin.getQueueProperties(queue.getName()) == null)) { if (logger.isDebugEnabled()) { diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java index 27b1824b94..71e1403ddf 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 the original author or authors. + * Copyright 2016-2023 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. @@ -539,16 +539,20 @@ public void testRecoverDeletedQueueNoAutoDeclare(BrokerRunningSupport brokerRunn private void testRecoverDeletedQueueGuts(boolean autoDeclare, BrokerRunningSupport brokerRunning) throws Exception { CachingConnectionFactory cf = new CachingConnectionFactory("localhost"); DirectMessageListenerContainer container = new DirectMessageListenerContainer(cf); + GenericApplicationContext context = new GenericApplicationContext(); + RabbitAdmin rabbitAdmin = new RabbitAdmin(cf); if (autoDeclare) { - GenericApplicationContext context = new GenericApplicationContext(); context.getBeanFactory().registerSingleton("foo", new Queue(Q1)); - RabbitAdmin rabbitAdmin = new RabbitAdmin(cf); - rabbitAdmin.setApplicationContext(context); - context.getBeanFactory().registerSingleton("admin", rabbitAdmin); - context.refresh(); - container.setApplicationContext(context); } - container.setAutoDeclare(autoDeclare); + else { + rabbitAdmin.setRedeclareManualDeclarations(true); + rabbitAdmin.declareQueue(new Queue(Q1)); + } + rabbitAdmin.setApplicationContext(context); + context.refresh(); + container.setApplicationContext(context); + + container.setAmqpAdmin(rabbitAdmin); container.setQueueNames(Q1, Q2); container.setConsumersPerQueue(2); container.setConsumersPerQueue(2); @@ -565,11 +569,6 @@ private void testRecoverDeletedQueueGuts(boolean autoDeclare, BrokerRunningSuppo assertThat(consumersOnQueue(Q2, 2)).isTrue(); assertThat(activeConsumerCount(container, 2)).isTrue(); assertThat(restartConsumerCount(container, 2)).isTrue(); - RabbitAdmin rabbitAdmin = new RabbitAdmin(cf); - if (!autoDeclare) { - Thread.sleep(2000); - rabbitAdmin.declareQueue(new Queue(Q1)); - } assertThat(consumersOnQueue(Q1, 2)).isTrue(); assertThat(consumersOnQueue(Q2, 2)).isTrue(); assertThat(activeConsumerCount(container, 4)).isTrue(); From 5451bfe000e7f2a8efecf4e0df7bdfd1915e9148 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 22 May 2023 17:07:05 -0400 Subject: [PATCH 239/737] GH-2456: Suppress Duplicate Annotations with Spy Resolves https://github.com/spring-projects/spring-amqp/issues/2456 When spying a `@RabbitListener` bean, duplicate methods are resolved as well as duplicate class level `@RabbitListener` annotations. **cherry-pick to 2.4.x** (will require instanceof polishing for Java 8) --- ...abbitListenerAnnotationBeanPostProcessor.java | 16 ++++++++++++++-- .../annotation/EnableRabbitIntegrationTests.java | 11 ++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java index 02a76135a7..1a04d214bd 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2023 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. @@ -339,7 +339,8 @@ private TypeMetadata buildMetadata(Class targetClass) { multiMethods.add(method); } } - }, ReflectionUtils.USER_DECLARED_METHODS); + }, ReflectionUtils.USER_DECLARED_METHODS + .and(meth -> !meth.getDeclaringClass().getName().contains("$MockitoMock$"))); if (methods.isEmpty() && multiMethods.isEmpty()) { return TypeMetadata.EMPTY; } @@ -352,6 +353,17 @@ private TypeMetadata buildMetadata(Class targetClass) { private List findListenerAnnotations(AnnotatedElement element) { return MergedAnnotations.from(element, SearchStrategy.TYPE_HIERARCHY) .stream(RabbitListener.class) + .filter(tma -> { + Object source = tma.getSource(); + String name = ""; + if (source instanceof Class clazz) { + name = clazz.getName(); + } + else if (source instanceof Method method) { + name = method.getDeclaringClass().getName(); + } + return !name.contains("$MockitoMock$"); + }) .map(ann -> ann.synthesize()) .collect(Collectors.toList()); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java index af286a88cd..59808981ba 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2023 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. @@ -19,8 +19,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.willAnswer; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import java.io.IOException; import java.io.Serializable; @@ -389,6 +393,7 @@ public void multiListener() { rabbitTemplate.convertAndSend("multi.exch", "multi.rk", bar); assertThat(this.rabbitTemplate.receiveAndConvert("sendTo.replies")) .isEqualTo("CRASHCRASH Test reply from error handler"); + verify(this.multi, times(2)).bar(any()); bar.field = "bar"; Baz baz = new Baz(); baz.field = "baz"; @@ -404,7 +409,7 @@ public void multiListener() { this.rabbitTemplate.setAfterReceivePostProcessors(mpp); assertThat(rabbitTemplate.convertSendAndReceive("multi.exch", "multi.rk", qux)).isEqualTo("QUX: qux: multi.rk"); assertThat(beanMethodHeaders).hasSize(2); - assertThat(beanMethodHeaders.get(0)).isEqualTo("MultiListenerBean"); + assertThat(beanMethodHeaders.get(0)).contains("$MultiListenerBean"); assertThat(beanMethodHeaders.get(1)).isEqualTo("qux"); this.rabbitTemplate.removeAfterReceivePostProcessor(mpp); assertThat(rabbitTemplate.convertSendAndReceive("multi.exch.tx", "multi.rk.tx", bar)).isEqualTo("BAR: barbar"); @@ -1978,7 +1983,7 @@ public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) { @Bean public MultiListenerBean multiListener() { - return new MultiListenerBean(); + return spy(new MultiListenerBean()); } @Bean From 6b720bbd22aca6d6c0f3b9934432b568154e919a Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Tue, 23 May 2023 18:06:34 +0200 Subject: [PATCH 240/737] Docs; Align client connection order reference Mention an `addressShuffleMode` option when describing a fail over connection factory behavior. --- src/reference/asciidoc/amqp.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index ccb8ab665a..50da8cd420 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -745,7 +745,7 @@ See <> for more information about ensuring message delivery. When using HA queues in a cluster, for the best performance, you may want to connect to the physical broker where the lead queue resides. The `CachingConnectionFactory` can be configured with multiple broker addresses. -This is to fail over and the client attempts to connect in order. +This is to fail over and the client attempts to connect in accordance with the configured `AddressShuffleMode` order. The `LocalizedQueueConnectionFactory` uses the REST API provided by the management plugin to determine which node is the lead for the queue. It then creates (or retrieves from a cache) a `CachingConnectionFactory` that connects to just that node. If the connection fails, the new lead node is determined and the consumer connects to it. From e452af157e724fb79312eb1be0fc3e2971a8b5b5 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 25 May 2023 10:58:14 -0400 Subject: [PATCH 241/737] GH-1210: Add Kotlin suspend functions support (#2460) * GH-1210: Add Kotlin suspend functions support Fixes https://github.com/spring-projects/spring-amqp/issues/1210 Kotlin Coroutines are essentially `Future` wrapping. Therefore, it is natural to have `suspend` support on `@RabbitListener` methods as we do now for `CompletableFuture` and `Mono` * Introduce some utilities since we cannot reuse existing from Spring Messaging: they are there about Kotlin Coroutines only for reactive handlers * Some code clean up in the `RabbitListenerAnnotationBeanPostProcessor` for the latest Java * Add optional dep for `kotlinx-coroutines-reactor` and document the feature * * Remove unused import --- build.gradle | 2 + ...itListenerAnnotationBeanPostProcessor.java | 126 +++------------ .../AbstractAdaptableMessageListener.java | 8 +- .../AmqpMessageHandlerMethodFactory.java | 143 ++++++++++++++++++ ...inuationHandlerMethodArgumentResolver.java | 50 ++++++ .../KotlinAwareInvocableHandlerMethod.java | 49 ++++++ .../annotation/EnableRabbitKotlinTests.kt | 26 ++-- src/reference/asciidoc/amqp.adoc | 6 +- src/reference/asciidoc/whats-new.adoc | 8 +- 9 files changed, 289 insertions(+), 129 deletions(-) create mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AmqpMessageHandlerMethodFactory.java create mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/ContinuationHandlerMethodArgumentResolver.java create mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/KotlinAwareInvocableHandlerMethod.java diff --git a/build.gradle b/build.gradle index f0d3686afc..9b2c53c036 100644 --- a/build.gradle +++ b/build.gradle @@ -56,6 +56,7 @@ ext { jaywayJsonPathVersion = '2.7.0' junit4Version = '4.13.2' junitJupiterVersion = '5.9.2' + kotlinCoroutinesVersion = '1.6.4' log4jVersion = '2.19.0' logbackVersion = '1.4.4' lz4Version = '1.8.0' @@ -436,6 +437,7 @@ project('spring-rabbit') { } optionalApi "com.jayway.jsonpath:json-path:$jaywayJsonPathVersion" optionalApi "org.apache.commons:commons-pool2:$commonsPoolVersion" + optionalApi "org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlinCoroutinesVersion" testApi project(':spring-rabbit-junit') testImplementation("com.willowtreeapps.assertk:assertk-jvm:$assertkVersion") diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java index 1a04d214bd..ffdd652657 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java @@ -18,8 +18,6 @@ import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -30,7 +28,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -56,6 +53,7 @@ import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory; import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistrar; import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry; +import org.springframework.amqp.rabbit.listener.adapter.AmqpMessageHandlerMethodFactory; import org.springframework.amqp.rabbit.listener.adapter.ReplyPostProcessor; import org.springframework.amqp.rabbit.listener.api.RabbitListenerErrorHandler; import org.springframework.amqp.support.converter.MessageConverter; @@ -76,7 +74,6 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.context.EnvironmentAware; import org.springframework.context.expression.StandardBeanExpressionResolver; -import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.MergedAnnotations; @@ -88,12 +85,9 @@ import org.springframework.core.task.TaskExecutor; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.lang.Nullable; -import org.springframework.messaging.Message; import org.springframework.messaging.converter.GenericMessageConverter; import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory; import org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory; -import org.springframework.messaging.handler.annotation.support.MethodArgumentNotValidException; -import org.springframework.messaging.handler.annotation.support.PayloadMethodArgumentResolver; import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; import org.springframework.util.Assert; @@ -101,8 +95,6 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; -import org.springframework.validation.BindingResult; -import org.springframework.validation.ObjectError; import org.springframework.validation.Validator; /** @@ -440,14 +432,10 @@ protected Collection processListener(MethodRabbitListenerEndpoint en List resolvedQueues = resolveQueues(rabbitListener, declarables); if (!resolvedQueues.isEmpty()) { if (resolvedQueues.get(0) instanceof String) { - endpoint.setQueueNames(resolvedQueues.stream() - .map(o -> (String) o) - .collect(Collectors.toList()).toArray(new String[0])); + endpoint.setQueueNames(resolvedQueues.stream().map(o -> (String) o).toArray(String[]::new)); } else { - endpoint.setQueues(resolvedQueues.stream() - .map(o -> (Queue) o) - .collect(Collectors.toList()).toArray(new Queue[0])); + endpoint.setQueues(resolvedQueues.stream().map(o -> (Queue) o).toArray(Queue[]::new)); } } endpoint.setConcurrency(resolveExpressionAsStringOrInteger(rabbitListener.concurrency(), "concurrency")); @@ -667,12 +655,10 @@ private List resolveQueues(RabbitListener rabbitListener, Collection queueNames = new ArrayList(); - List queueBeans = new ArrayList(); - if (queues.length > 0) { - for (int i = 0; i < queues.length; i++) { - resolveQueues(queues[i], queueNames, queueBeans); - } + List queueNames = new ArrayList<>(); + List queueBeans = new ArrayList<>(); + for (String queue : queues) { + resolveQueues(queue, queueNames, queueBeans); } if (!queueNames.isEmpty()) { // revert to the previous behavior of just using the name when there is mixture of String and Queue @@ -684,8 +670,8 @@ private List resolveQueues(RabbitListener rabbitListener, Collection 0) { @@ -755,7 +741,7 @@ private String[] registerBeansForDeclaration(RabbitListener rabbitListener, Coll declareExchangeAndBinding(binding, queueName, declarables); } } - return queues.toArray(new String[queues.size()]); + return queues.toArray(new String[0]); } private String declareQueue(org.springframework.amqp.rabbit.annotation.Queue bindingQueue, @@ -862,7 +848,7 @@ private void registerBindings(QueueBinding binding, String queueName, String exc } private Map resolveArguments(Argument[] arguments) { - Map map = new HashMap(); + Map map = new HashMap<>(); for (Argument arg : arguments) { String key = resolveExpressionAsString(arg.name(), "@Argument.name"); if (StringUtils.hasText(key)) { @@ -1027,7 +1013,7 @@ private MessageHandlerMethodFactory getFactory() { } private MessageHandlerMethodFactory createDefaultMessageHandlerMethodFactory() { - DefaultMessageHandlerMethodFactory defaultFactory = new DefaultMessageHandlerMethodFactory(); + DefaultMessageHandlerMethodFactory defaultFactory = new AmqpMessageHandlerMethodFactory(); Validator validator = RabbitListenerAnnotationBeanPostProcessor.this.registrar.getValidator(); if (validator != null) { defaultFactory.setValidator(validator); @@ -1040,74 +1026,14 @@ private MessageHandlerMethodFactory createDefaultMessageHandlerMethodFactory() { List customArgumentsResolver = new ArrayList<>( RabbitListenerAnnotationBeanPostProcessor.this.registrar.getCustomMethodArgumentResolvers()); defaultFactory.setCustomArgumentResolvers(customArgumentsResolver); - GenericMessageConverter messageConverter = new GenericMessageConverter( - this.defaultFormattingConversionService); - defaultFactory.setMessageConverter(messageConverter); - // Has to be at the end - look at PayloadMethodArgumentResolver documentation - customArgumentsResolver.add(new OptionalEmptyAwarePayloadArgumentResolver(messageConverter, validator)); + defaultFactory.setMessageConverter(new GenericMessageConverter(this.defaultFormattingConversionService)); + defaultFactory.afterPropertiesSet(); return defaultFactory; } } - private static class OptionalEmptyAwarePayloadArgumentResolver extends PayloadMethodArgumentResolver { - - OptionalEmptyAwarePayloadArgumentResolver( - org.springframework.messaging.converter.MessageConverter messageConverter, - @Nullable Validator validator) { - - super(messageConverter, validator); - } - - @Override - public Object resolveArgument(MethodParameter parameter, Message message) throws Exception { // NOSONAR - Object resolved = null; - try { - resolved = super.resolveArgument(parameter, message); - } - catch (MethodArgumentNotValidException ex) { - Type type = parameter.getGenericParameterType(); - if (isOptional(message, type)) { - BindingResult bindingResult = ex.getBindingResult(); - if (bindingResult != null) { - List allErrors = bindingResult.getAllErrors(); - if (allErrors.size() == 1) { - String defaultMessage = allErrors.get(0).getDefaultMessage(); - if ("Payload value must not be empty".equals(defaultMessage)) { - return Optional.empty(); - } - } - } - } - throw ex; - } - /* - * Replace Optional.empty() list elements with null. - */ - if (resolved instanceof List) { - List list = ((List) resolved); - for (int i = 0; i < list.size(); i++) { - if (list.get(i).equals(Optional.empty())) { - list.set(i, null); - } - } - } - return resolved; - } - - private boolean isOptional(Message message, Type type) { - return (Optional.class.equals(type) || (type instanceof ParameterizedType pType - && Optional.class.equals(pType.getRawType()))) - && message.getPayload().equals(Optional.empty()); - } - - @Override - protected boolean isEmptyPayload(Object payload) { - return payload == null || payload.equals(Optional.empty()); - } - - } /** * The metadata holder of the class with {@link RabbitListener} * and {@link RabbitHandler} annotations. @@ -1147,28 +1073,14 @@ private TypeMetadata() { /** * A method annotated with {@link RabbitListener}, together with the annotations. + * + * @param method the method with annotations + * @param annotations on the method */ - private static class ListenerMethod { - - final Method method; // NOSONAR - - final RabbitListener[] annotations; // NOSONAR - - ListenerMethod(Method method, RabbitListener[] annotations) { // NOSONAR - this.method = method; - this.annotations = annotations; // NOSONAR - } - + private record ListenerMethod(Method method, RabbitListener[] annotations) { } - private static class BytesToStringConverter implements Converter { - - - private final Charset charset; - - BytesToStringConverter(Charset charset) { - this.charset = charset; - } + private record BytesToStringConverter(Charset charset) implements Converter { @Override public String convert(byte[] source) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java index 96b21afb41..6212b0405c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java @@ -364,7 +364,7 @@ protected void handleResult(InvocationResult resultArg, Message request, Channel * response message back. * @param resultArg the result object to handle (never null) * @param request the original request message - * @param channel the Rabbit channel to operate on (may be null) + * @param channel the Rabbit channel to operate on (maybe null) * @param source the source data for the method invocation - e.g. * {@code o.s.messaging.Message}; may be null * @see #buildMessage @@ -391,8 +391,8 @@ protected void handleResult(InvocationResult resultArg, Message request, Channel } else if (monoPresent && MonoHandler.isMono(resultArg.getReturnValue())) { if (!this.isManualAck) { - this.logger.warn("Container AcknowledgeMode must be MANUAL for a Mono return type; " - + "otherwise the container will ack the message immediately"); + this.logger.warn("Container AcknowledgeMode must be MANUAL for a Mono return type" + + "(or Kotlin suspend function); otherwise the container will ack the message immediately"); } MonoHandler.subscribe(resultArg.getReturnValue(), r -> asyncSuccess(resultArg, request, channel, source, r), @@ -448,7 +448,7 @@ private void basicAck(Message request, Channel channel) { } private void asyncFailure(Message request, Channel channel, Throwable t) { - this.logger.error("Future or Mono was completed with an exception for " + request, t); + this.logger.error("Future, Mono, or suspend function was completed with an exception for " + request, t); try { channel.basicNack(request.getMessageProperties().getDeliveryTag(), false, ContainerUtils.shouldRequeue(this.defaultRequeueRejected, t, this.logger)); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AmqpMessageHandlerMethodFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AmqpMessageHandlerMethodFactory.java new file mode 100644 index 0000000000..4ac0263a5b --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AmqpMessageHandlerMethodFactory.java @@ -0,0 +1,143 @@ +/* + * Copyright 2023 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 java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Optional; + +import org.springframework.core.KotlinDetector; +import org.springframework.core.MethodParameter; +import org.springframework.lang.Nullable; +import org.springframework.messaging.Message; +import org.springframework.messaging.converter.MessageConverter; +import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory; +import org.springframework.messaging.handler.annotation.support.MethodArgumentNotValidException; +import org.springframework.messaging.handler.annotation.support.PayloadMethodArgumentResolver; +import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; +import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolverComposite; +import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; +import org.springframework.validation.BindingResult; +import org.springframework.validation.ObjectError; +import org.springframework.validation.Validator; + +/** + * Extension of the {@link DefaultMessageHandlerMethodFactory} for Spring AMQP requirements. + * + * @author Artem Bilan + * + * @since 3.0.5 + */ +public class AmqpMessageHandlerMethodFactory extends DefaultMessageHandlerMethodFactory { + + private final HandlerMethodArgumentResolverComposite argumentResolvers = + new HandlerMethodArgumentResolverComposite(); + + private MessageConverter messageConverter; + + private Validator validator; + + @Override + public void setMessageConverter(MessageConverter messageConverter) { + super.setMessageConverter(messageConverter); + this.messageConverter = messageConverter; + } + + @Override + public void setValidator(Validator validator) { + super.setValidator(validator); + this.validator = validator; + } + + @Override + protected List initArgumentResolvers() { + List resolvers = super.initArgumentResolvers(); + if (KotlinDetector.isKotlinPresent()) { + // Insert before PayloadMethodArgumentResolver + resolvers.add(resolvers.size() - 1, new ContinuationHandlerMethodArgumentResolver()); + } + // Has to be at the end, but before PayloadMethodArgumentResolver + resolvers.add(resolvers.size() - 1, + new OptionalEmptyAwarePayloadArgumentResolver(this.messageConverter, this.validator)); + this.argumentResolvers.addResolvers(resolvers); + return resolvers; + } + + @Override + public InvocableHandlerMethod createInvocableHandlerMethod(Object bean, Method method) { + InvocableHandlerMethod handlerMethod = new KotlinAwareInvocableHandlerMethod(bean, method); + handlerMethod.setMessageMethodArgumentResolvers(this.argumentResolvers); + return handlerMethod; + } + + private static class OptionalEmptyAwarePayloadArgumentResolver extends PayloadMethodArgumentResolver { + + OptionalEmptyAwarePayloadArgumentResolver(MessageConverter messageConverter, @Nullable Validator validator) { + super(messageConverter, validator); + } + + @Override + public Object resolveArgument(MethodParameter parameter, Message message) throws Exception { // NOSONAR + Object resolved; + try { + resolved = super.resolveArgument(parameter, message); + } + catch (MethodArgumentNotValidException ex) { + Type type = parameter.getGenericParameterType(); + if (isOptional(message, type)) { + BindingResult bindingResult = ex.getBindingResult(); + if (bindingResult != null) { + List allErrors = bindingResult.getAllErrors(); + if (allErrors.size() == 1) { + String defaultMessage = allErrors.get(0).getDefaultMessage(); + if ("Payload value must not be empty".equals(defaultMessage)) { + return Optional.empty(); + } + } + } + } + throw ex; + } + /* + * Replace Optional.empty() list elements with null. + */ + if (resolved instanceof List list) { + for (int i = 0; i < list.size(); i++) { + if (list.get(i).equals(Optional.empty())) { + list.set(i, null); + } + } + } + return resolved; + } + + private boolean isOptional(Message message, Type type) { + return (Optional.class.equals(type) || + (type instanceof ParameterizedType pType && Optional.class.equals(pType.getRawType()))) + && message.getPayload().equals(Optional.empty()); + } + + @Override + protected boolean isEmptyPayload(Object payload) { + return payload == null || payload.equals(Optional.empty()); + } + + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/ContinuationHandlerMethodArgumentResolver.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/ContinuationHandlerMethodArgumentResolver.java new file mode 100644 index 0000000000..a9f2dcd87f --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/ContinuationHandlerMethodArgumentResolver.java @@ -0,0 +1,50 @@ +/* + * Copyright 2023 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 org.springframework.core.MethodParameter; +import org.springframework.messaging.Message; +import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; + +import reactor.core.publisher.Mono; + +/** + * No-op resolver for method arguments of type {@link kotlin.coroutines.Continuation}. + *

+ * This class is similar to + * {@link org.springframework.messaging.handler.annotation.reactive.ContinuationHandlerMethodArgumentResolver} + * but for regular {@link HandlerMethodArgumentResolver} contract. + * + * @author Artem Bilan + * + * @since 3.0.5 + * + * @see org.springframework.messaging.handler.annotation.reactive.ContinuationHandlerMethodArgumentResolver + */ +public class ContinuationHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return "kotlin.coroutines.Continuation".equals(parameter.getParameterType().getName()); + } + + @Override + public Object resolveArgument(MethodParameter parameter, Message message) { + return Mono.empty(); + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/KotlinAwareInvocableHandlerMethod.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/KotlinAwareInvocableHandlerMethod.java new file mode 100644 index 0000000000..a3506e3315 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/KotlinAwareInvocableHandlerMethod.java @@ -0,0 +1,49 @@ +/* + * Copyright 2023 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 java.lang.reflect.Method; + +import org.springframework.core.CoroutinesUtils; +import org.springframework.core.KotlinDetector; +import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; + +/** + * An {@link InvocableHandlerMethod} extension for supporting Kotlin {@code suspend} function. + * + * @author Artem Bilan + * + * @since 3.0.5 + */ +public class KotlinAwareInvocableHandlerMethod extends InvocableHandlerMethod { + + public KotlinAwareInvocableHandlerMethod(Object bean, Method method) { + super(bean, method); + } + + @Override + protected Object doInvoke(Object... args) throws Exception { + Method method = getBridgedMethod(); + if (KotlinDetector.isSuspendingFunction(method)) { + return CoroutinesUtils.invokeSuspendingFunction(method, getBean(), args); + } + else { + return super.doInvoke(args); + } + } + +} diff --git a/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt b/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt index 9960d52cad..981b284861 100644 --- a/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt +++ b/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt @@ -20,6 +20,7 @@ import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isTrue import org.junit.jupiter.api.Test +import org.springframework.amqp.core.AcknowledgeMode import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory import org.springframework.amqp.rabbit.connection.CachingConnectionFactory import org.springframework.amqp.rabbit.core.RabbitTemplate @@ -56,14 +57,14 @@ class EnableRabbitKotlinTests { private lateinit var config: Config @Test - fun `send and wait for consume` () { + fun `send and wait for consume`() { val template = RabbitTemplate(this.config.cf()) template.convertAndSend("kotlinQueue", "test") assertThat(this.config.latch.await(10, TimeUnit.SECONDS)).isTrue(); } @Test - fun `send and wait for consume with EH` () { + fun `send and wait for consume with EH`() { val template = RabbitTemplate(this.config.cf()) template.convertAndSend("kotlinQueue1", "test") assertThat(this.config.ehLatch.await(10, TimeUnit.SECONDS)).isTrue(); @@ -78,27 +79,22 @@ class EnableRabbitKotlinTests { val latch = CountDownLatch(1) @RabbitListener(queues = ["kotlinQueue"]) - fun handle(@Suppress("UNUSED_PARAMETER") data: String) { + suspend fun handle(@Suppress("UNUSED_PARAMETER") data: String) { this.latch.countDown() } @Bean - fun rabbitListenerContainerFactory(cf: CachingConnectionFactory): SimpleRabbitListenerContainerFactory { - val factory = SimpleRabbitListenerContainerFactory() - factory.setConnectionFactory(cf) - return factory - } + fun rabbitListenerContainerFactory(cf: CachingConnectionFactory) = + SimpleRabbitListenerContainerFactory().also { + it.setAcknowledgeMode(AcknowledgeMode.MANUAL) + it.setConnectionFactory(cf) + } @Bean - fun cf(): CachingConnectionFactory { - return CachingConnectionFactory( - RabbitAvailableCondition.getBrokerRunning().connectionFactory) - } + fun cf() = CachingConnectionFactory(RabbitAvailableCondition.getBrokerRunning().connectionFactory) @Bean - fun multi(): Multi { - return Multi() - } + fun multi() = Multi() @Bean fun proxyListenerPostProcessor(): BeanPostProcessor? { diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index 50da8cd420..75d0ae1a2e 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -3085,7 +3085,7 @@ Each queue needed a separate property. ====== 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 the address specified in the `ReplyToAddress` header of the original message, or to the default address configured on the listener. +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. Assuming our `processOrder` method should now return an `OrderStatus`, we can write it as follows to automatically send a reply: @@ -3660,6 +3660,10 @@ If some exception occurs within the listener method that prevents creation of th 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. +Starting with version 3.0.5, the `@RabbitListener` (and `@RabbitHandler`) methods can be marked with Kotlin `suspend` and the whole handling process and reply producing (optional) happens on respective Kotlin coroutine. +All the mentioned rules about `AcknowledgeMode.MANUAL` are still apply. +The `org.jetbrains.kotlinx:kotlinx-coroutines-reactor` dependency must be present in classpath to allow `suspend` function invocations. + [[threading]] ===== Threading and Asynchronous Consumers diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index 1e5cd57eeb..cb6d4ae38c 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -40,7 +40,7 @@ See <> for more information. 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 <> for more infoprmation. +See <> for more information. `MessageConverter` s can now return `Optional.empty()` for a null value; this is currently implemented by the `Jackson2JsonMessageConverter`. See <> for more information @@ -48,9 +48,13 @@ See <> for more information You can now configure a `ReplyPostProcessor` via the container factory rather than via a property on `@RabbitListener`. See <> for more information. +The `@RabbitListener` (and `@RabbitHandler`) methods can now be as a Kotlin `suspend` functions. +See <> for more information. + ==== Connection Factory Changes -The default `addressShuffleMode` in `AbstractConnectionFactory` is now `RANDOM`. This results in connecting to a random host when multiple addresses are provided. +The default `addressShuffleMode` in `AbstractConnectionFactory` is now `RANDOM`. +This results in connecting to a random host when multiple addresses are provided. See <> for more information. The `LocalizedQueueConnectionFactory` no longer uses the RabbitMQ `http-client` library to determine which node is the leader for a queue. From e492b98bf875b885c2db3fc4dbb4154a8a6b4fed Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 25 May 2023 13:12:19 -0400 Subject: [PATCH 242/737] GH-2461: RabbitListenerErrorHandler with Async Resolves https://github.com/spring-projects/spring-amqp/issues/2461 --- .../AbstractAdaptableMessageListener.java | 8 ++++---- .../adapter/MessagingMessageListenerAdapter.java | 16 +++++++++++++++- .../rabbit/annotation/EnableRabbitKotlinTests.kt | 4 ++-- src/reference/asciidoc/amqp.adoc | 3 +++ src/reference/asciidoc/whats-new.adoc | 5 ++++- 5 files changed, 28 insertions(+), 8 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java index 6212b0405c..5f7215c49a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2023 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. @@ -385,7 +385,7 @@ protected void handleResult(InvocationResult resultArg, Message request, Channel basicAck(request, channel); } else { - asyncFailure(request, channel, t); + asyncFailure(request, channel, t, source); } }); } @@ -396,7 +396,7 @@ else if (monoPresent && MonoHandler.isMono(resultArg.getReturnValue())) { } MonoHandler.subscribe(resultArg.getReturnValue(), r -> asyncSuccess(resultArg, request, channel, source, r), - t -> asyncFailure(request, channel, t), + t -> asyncFailure(request, channel, t, source), () -> basicAck(request, channel)); } else { @@ -447,7 +447,7 @@ private void basicAck(Message request, Channel channel) { } } - private void asyncFailure(Message request, Channel channel, Throwable t) { + protected void asyncFailure(Message request, Channel channel, Throwable t, Object source) { this.logger.error("Future, Mono, or suspend function was completed with an exception for " + request, t); try { channel.basicNack(request.getMessageProperties().getDeliveryTag(), false, diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java index d38bab75cf..473d2b5bd3 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -160,6 +160,20 @@ public void onMessage(org.springframework.amqp.core.Message amqpMessage, Channel } } + @Override + protected void asyncFailure(org.springframework.amqp.core.Message request, Channel channel, Throwable t, + Object source) { + + try { + handleException(request, channel, (Message) source, + new ListenerExecutionFailedException("Async Fail", t, request)); + return; + } + catch (Exception ex) { + } + super.asyncFailure(request, channel, t, source); + } + private void handleException(org.springframework.amqp.core.Message amqpMessage, Channel channel, @Nullable Message message, ListenerExecutionFailedException e) throws Exception { // NOSONAR diff --git a/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt b/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt index 981b284861..2fc695faf2 100644 --- a/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt +++ b/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2021 the original author or authors. + * Copyright 2018-2023 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. @@ -126,7 +126,7 @@ class EnableRabbitKotlinTests { open class Multi { @RabbitHandler - fun handle(@Suppress("UNUSED_PARAMETER") data: String) { + suspend fun handle(@Suppress("UNUSED_PARAMETER") data: String) { throw RuntimeException("fail") } diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index 75d0ae1a2e..3a8727a337 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -3664,6 +3664,9 @@ Starting with version 3.0.5, the `@RabbitListener` (and `@RabbitHandler`) method All the mentioned rules about `AcknowledgeMode.MANUAL` are still apply. The `org.jetbrains.kotlinx:kotlinx-coroutines-reactor` dependency must be present in classpath to allow `suspend` function invocations. +Also starting with version 3.0.5, if a `RabbitListenerErrorHandler` is configured on a listener with an async return type (including Kotlin suspend functions), the error handler is invoked after a failure. +See <> for more information about this error handler and its purpose. + [[threading]] ===== Threading and Asynchronous Consumers diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index cb6d4ae38c..d386714b5c 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -48,9 +48,12 @@ See <> for more information You can now configure a `ReplyPostProcessor` via the container factory rather than via a property on `@RabbitListener`. See <> for more information. -The `@RabbitListener` (and `@RabbitHandler`) methods can now be as a Kotlin `suspend` functions. +The `@RabbitListener` (and `@RabbitHandler`) methods can now be declared as Kotlin `suspend` functions. See <> for more information. +Starting with version 3.0.5, listeners with async return types (including Kotlin suspend functions) invoke the `RabbitListenerErrorHandler` (if configured) after a failure. +Previously, the error handler was only invoked with synchronous invocations. + ==== Connection Factory Changes The default `addressShuffleMode` in `AbstractConnectionFactory` is now `RANDOM`. From 6b4b908bf4487aaa9d6ba98883edc5350f010bf3 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 25 May 2023 16:29:36 -0400 Subject: [PATCH 243/737] GH-2451: Add StreamAdmin Resolves https://github.com/spring-projects/spring-amqp/issues/2451 **cherry-pick to 2.4.x** --- .../rabbit/stream/support/StreamAdmin.java | 100 ++++++++++++++++++ .../stream/listener/RabbitListenerTests.java | 18 +++- src/reference/asciidoc/stream.adoc | 34 +++++- 3 files changed, 146 insertions(+), 6 deletions(-) create mode 100644 spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/StreamAdmin.java diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/StreamAdmin.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/StreamAdmin.java new file mode 100644 index 0000000000..dc6e336a7a --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/StreamAdmin.java @@ -0,0 +1,100 @@ +/* + * Copyright 2023 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.rabbit.stream.support; + +import java.util.function.Consumer; + +import org.springframework.context.SmartLifecycle; +import org.springframework.util.Assert; + +import com.rabbitmq.stream.Environment; +import com.rabbitmq.stream.StreamCreator; + +/** + * Used to provision streams. + * + * @author Gary Russell + * @since 2.4.13 + * + */ +public class StreamAdmin implements SmartLifecycle { + + private final StreamCreator streamCreator; + + private final Consumer callback; + + private boolean autoStartup = true; + + private int phase; + + private volatile boolean running; + + /** + * Construct with the provided parameters. + * @param env the environment. + * @param callback the callback to receive the {@link StreamCreator}. + */ + public StreamAdmin(Environment env, Consumer callback) { + Assert.notNull(env, "Environment cannot be null"); + Assert.notNull(callback, "'callback' cannot be null"); + this.streamCreator = env.streamCreator(); + this.callback = callback; + } + + @Override + public int getPhase() { + return this.phase; + } + + /** + * Set the phase; default is 0. + * @param phase the phase. + */ + public void setPhase(int phase) { + this.phase = phase; + } + + /** + * Set to false to prevent automatic startup. + * @param autoStartup the autoStartup. + */ + public void setAutoStartup(boolean autoStartup) { + this.autoStartup = autoStartup; + } + + @Override + public void start() { + this.callback.accept(this.streamCreator); + this.running = true; + } + + @Override + public void stop() { + this.running = false; + } + + @Override + public boolean isRunning() { + return this.running; + } + + @Override + public boolean isAutoStartup() { + return this.autoStartup; + } + +} diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java index 0ea7fc50ea..d15fbdb164 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 the original author or authors. + * Copyright 2021-2023 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. @@ -52,6 +52,7 @@ import org.springframework.rabbit.stream.config.StreamRabbitListenerContainerFactory; import org.springframework.rabbit.stream.producer.RabbitStreamTemplate; import org.springframework.rabbit.stream.retry.StreamRetryOperationsInterceptorFactoryBean; +import org.springframework.rabbit.stream.support.StreamAdmin; import org.springframework.rabbit.stream.support.StreamMessageProperties; import org.springframework.retry.interceptor.RetryOperationsInterceptor; import org.springframework.test.annotation.DirtiesContext; @@ -170,7 +171,17 @@ static Environment environment() { } @Bean - SmartLifecycle creator(Environment env) { + StreamAdmin streamAdmin(Environment env) { + StreamAdmin streamAdmin = new StreamAdmin(env, sc -> { + sc.stream("test.stream.queue1").create(); + sc.stream("test.stream.queue2").create(); + }); + streamAdmin.setAutoStartup(false); + return streamAdmin; + } + + @Bean + SmartLifecycle creator(Environment env, StreamAdmin admin) { return new SmartLifecycle() { boolean running; @@ -184,8 +195,7 @@ public void stop() { @Override public void start() { clean(env); - env.streamCreator().stream("test.stream.queue1").create(); - env.streamCreator().stream("test.stream.queue2").create(); + admin.start(); this.running = true; } diff --git a/src/reference/asciidoc/stream.adoc b/src/reference/asciidoc/stream.adoc index ca3e07a980..264e58e45d 100644 --- a/src/reference/asciidoc/stream.adoc +++ b/src/reference/asciidoc/stream.adoc @@ -28,8 +28,38 @@ compile 'org.springframework.amqp:spring-rabbit-stream:{project-version}' ---- ==== -Provision the queues as normal, using a `RabbitAdmin` bean, using the `QueueBuilder.stream()` method to designate the queue type. -See <>. +You can provision the queues as normal, using a `RabbitAdmin` bean, using the `QueueBuilder.stream()` method to designate the queue type. +For example: + +==== +[source, java] +---- +@Bean +Queue stream() { + return QueueBuilder.durable("stream.queue1") + .stream() + .build(); +} +---- +==== + +However, this will only work if you are also using non-stream components (such as the `SimpleMessageListenerContainer` or `DirectMessageListenerContainer`) because the admin is triggered to declare the defined beans when an AMQP connection is opened. +If your application only uses stream components, or you wish to use advanced stream configuration features, you should configure a `StreamAdmin` instead: + +==== +[source, java] +---- +@Bean +StreamAdmin streamAdmin(Environment env) { + return new StreamAdmin(env, sc -> { + sc.stream("stream.queue1").maxAge(Duration.ofHours(2)).create(); + sc.stream("stream.queue2").create(); + }); +} +---- +==== + +Refer to the RabbitMQ documentation for more information about the `StreamCreator`. ==== Sending Messages From 974a4a915fde97137a318a76ef739235fcb14c74 Mon Sep 17 00:00:00 2001 From: Toshiaki Maki Date: Tue, 30 May 2023 23:33:23 +0900 Subject: [PATCH 244/737] Fix typo in stream.adoc `ConvertableFuture` -> `CompletableFuture` --- src/reference/asciidoc/stream.adoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/reference/asciidoc/stream.adoc b/src/reference/asciidoc/stream.adoc index 264e58e45d..878a036617 100644 --- a/src/reference/asciidoc/stream.adoc +++ b/src/reference/asciidoc/stream.adoc @@ -71,13 +71,13 @@ The `RabbitStreamTemplate` provides a subset of the `RabbitTemplate` (AMQP) func ---- public interface RabbitStreamOperations extends AutoCloseable { - ConvertableFuture send(Message message); + CompletableFuture send(Message message); - ConvertableFuture convertAndSend(Object message); + CompletableFuture convertAndSend(Object message); - ConvertableFuture convertAndSend(Object message, @Nullable MessagePostProcessor mpp); + CompletableFuture convertAndSend(Object message, @Nullable MessagePostProcessor mpp); - ConvertableFuture send(com.rabbitmq.stream.Message message); + CompletableFuture send(com.rabbitmq.stream.Message message); MessageBuilder messageBuilder(); From a75b3d0837ed8f1d7ae956aa3fdb443de3c769b7 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 1 Jun 2023 12:05:54 -0400 Subject: [PATCH 245/737] GH-2467: Micrometer Observation for Streams Resolves https://github.com/spring-projects/spring-amqp/issues/2467 * Fix race in test. * Add tracing test; fix possible NPEs in stream contexts. * Change input dir for doc generation. * Strip package from new conventions in gen'd doc. * Docs; polishing; fix doc generation for duplicate enum values. * Use stream convention, regardless of native listener or not. --- build.gradle | 9 +- .../StreamRabbitListenerContainerFactory.java | 23 +- .../listener/StreamListenerContainer.java | 129 +++++++---- .../RabbitStreamListenerObservation.java | 101 ++++++++ ...itStreamListenerObservationConvention.java | 42 ++++ .../RabbitStreamMessageReceiverContext.java | 102 ++++++++ .../RabbitStreamMessageSenderContext.java | 62 +++++ .../RabbitStreamTemplateObservation.java | 103 +++++++++ ...itStreamTemplateObservationConvention.java | 42 ++++ .../stream/micrometer/package-info.java | 6 + .../stream/producer/RabbitStreamTemplate.java | 81 ++++++- .../DefaultStreamMessageConverter.java | 2 +- .../stream/listener/RabbitListenerTests.java | 91 +++++++- .../stream/micrometer/TracingTests.java | 218 ++++++++++++++++++ ...bstractRabbitListenerContainerFactory.java | 43 +--- .../BaseRabbitListenerContainerFactory.java | 52 ++++- .../amqp/rabbit/core/RabbitTemplate.java | 2 +- .../AbstractMessageListenerContainer.java | 129 ++--------- .../rabbit/listener/MicrometerHolder.java | 8 +- .../listener/ObservableListenerContainer.java | 155 +++++++++++++ src/reference/asciidoc/amqp.adoc | 3 +- src/reference/asciidoc/stream.adoc | 19 ++ 22 files changed, 1201 insertions(+), 221 deletions(-) create mode 100644 spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamListenerObservation.java create mode 100644 spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamListenerObservationConvention.java create mode 100644 spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamMessageReceiverContext.java create mode 100644 spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamMessageSenderContext.java create mode 100644 spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamTemplateObservation.java create mode 100644 spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamTemplateObservationConvention.java create mode 100644 spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/package-info.java create mode 100644 spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/micrometer/TracingTests.java create mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ObservableListenerContainer.java diff --git a/build.gradle b/build.gradle index 9b2c53c036..5fa5ce875e 100644 --- a/build.gradle +++ b/build.gradle @@ -474,6 +474,7 @@ project('spring-rabbit-stream') { api project(':spring-rabbit') api "com.rabbitmq:stream-client:$rabbitmqStreamVersion" + optionalApi 'io.micrometer:micrometer-core' testApi project(':spring-rabbit-junit') testRuntimeOnly 'com.fasterxml.jackson.core:jackson-core' @@ -488,6 +489,10 @@ project('spring-rabbit-stream') { testImplementation "org.testcontainers:junit-jupiter" testImplementation "org.apache.logging.log4j:log4j-slf4j-impl:$log4jVersion" testImplementation 'org.springframework:spring-webflux' + testImplementation 'io.micrometer:micrometer-observation-test' + testImplementation 'io.micrometer:micrometer-tracing-bridge-brave' + testImplementation 'io.micrometer:micrometer-tracing-test' + testImplementation 'io.micrometer:micrometer-tracing-integration-test' } } @@ -547,7 +552,7 @@ task prepareAsciidocBuild(type: Sync) { into "$buildDir/asciidoc" } -def observationInputDir = file('spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer').absolutePath +def observationInputDir = project.rootDir.absolutePath def generatedDocsDir = file("$buildDir/docs/generated").absolutePath task generateObservabilityDocs(type: JavaExec) { @@ -564,7 +569,7 @@ task filterMetricsDocsContent(type: Copy) { include '_*.adoc' into generatedDocsDir rename { filename -> filename.replace '_', '' } - filter { line -> line.replaceAll('org.springframework.amqp.rabbit.support.micrometer.', '').replaceAll('^Fully qualified n', 'N') } + filter { line -> line.replaceAll('org.springframework.*.micrometer.', '').replaceAll('^Fully qualified n', 'N') } } asciidoctorPdf { diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/StreamRabbitListenerContainerFactory.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/StreamRabbitListenerContainerFactory.java index 0eb337abfd..7b8aa73a2d 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/StreamRabbitListenerContainerFactory.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/StreamRabbitListenerContainerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 the original author or authors. + * Copyright 2021-2023 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. @@ -31,6 +31,7 @@ import org.springframework.rabbit.stream.listener.ConsumerCustomizer; import org.springframework.rabbit.stream.listener.StreamListenerContainer; import org.springframework.rabbit.stream.listener.adapter.StreamMessageListenerAdapter; +import org.springframework.rabbit.stream.micrometer.RabbitStreamListenerObservationConvention; import org.springframework.util.Assert; import com.rabbitmq.stream.Environment; @@ -53,6 +54,8 @@ public class StreamRabbitListenerContainerFactory private ContainerCustomizer containerCustomizer; + private RabbitStreamListenerObservationConvention streamListenerObservationConvention; + /** * Construct an instance using the provided environment. * @param environment the environment. @@ -87,6 +90,18 @@ public void setContainerCustomizer(ContainerCustomizer this.containerCustomizer = containerCustomizer; } + /** + * Set a {@link RabbitStreamListenerObservationConvention} that is used when receiving + * native stream messages. + * @param streamListenerObservationConvention the convention. + * @since 3.0.5 + */ + public void setStreamListenerObservationConvention( + RabbitStreamListenerObservationConvention streamListenerObservationConvention) { + + this.streamListenerObservationConvention = streamListenerObservationConvention; + } + @Override public StreamListenerContainer createListenerContainer(RabbitListenerEndpoint endpoint) { if (endpoint instanceof MethodRabbitListenerEndpoint && this.nativeListener) { @@ -101,8 +116,12 @@ public StreamListenerContainer createListenerContainer(RabbitListenerEndpoint en StreamListenerContainer container = createContainerInstance(); Advice[] adviceChain = getAdviceChain(); JavaUtils.INSTANCE + .acceptIfNotNull(getApplicationContext(), container::setApplicationContext) .acceptIfNotNull(this.consumerCustomizer, container::setConsumerCustomizer) - .acceptIfNotNull(adviceChain, container::setAdviceChain); + .acceptIfNotNull(adviceChain, container::setAdviceChain) + .acceptIfNotNull(getMicrometerEnabled(), container::setMicrometerEnabled) + .acceptIfNotNull(getObservationEnabled(), container::setObservationEnabled) + .acceptIfNotNull(this.streamListenerObservationConvention, container::setObservationConvention); applyCommonOverrides(endpoint, container); if (this.containerCustomizer != null) { this.containerCustomizer.configure(container); diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java index ea5d2fe8d1..3f55623d1c 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 the original author or authors. + * Copyright 2021-2023 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. @@ -25,13 +25,18 @@ import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageListener; -import org.springframework.amqp.rabbit.listener.MessageListenerContainer; +import org.springframework.amqp.rabbit.listener.MicrometerHolder; +import org.springframework.amqp.rabbit.listener.ObservableListenerContainer; import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener; +import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.support.DefaultPointcutAdvisor; -import org.springframework.beans.factory.BeanNameAware; import org.springframework.core.log.LogAccessor; import org.springframework.lang.Nullable; +import org.springframework.rabbit.stream.micrometer.RabbitStreamListenerObservation; +import org.springframework.rabbit.stream.micrometer.RabbitStreamListenerObservation.DefaultRabbitStreamListenerObservationConvention; +import org.springframework.rabbit.stream.micrometer.RabbitStreamListenerObservationConvention; +import org.springframework.rabbit.stream.micrometer.RabbitStreamMessageReceiverContext; import org.springframework.rabbit.stream.support.StreamMessageProperties; import org.springframework.rabbit.stream.support.converter.DefaultStreamMessageConverter; import org.springframework.rabbit.stream.support.converter.StreamMessageConverter; @@ -41,6 +46,8 @@ import com.rabbitmq.stream.Consumer; import com.rabbitmq.stream.ConsumerBuilder; import com.rabbitmq.stream.Environment; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; /** * A listener container for RabbitMQ Streams. @@ -49,7 +56,7 @@ * @since 2.4 * */ -public class StreamListenerContainer implements MessageListenerContainer, BeanNameAware { +public class StreamListenerContainer extends ObservableListenerContainer { protected LogAccessor logger = new LogAccessor(LogFactory.getLog(getClass())); // NOSONAR @@ -67,10 +74,6 @@ public class StreamListenerContainer implements MessageListenerContainer, BeanNa private int concurrency = 1; - private String listenerId; - - private String beanName; - private boolean autoStartup = true; private MessageListener messageListener; @@ -79,6 +82,11 @@ public class StreamListenerContainer implements MessageListenerContainer, BeanNa private Advice[] adviceChain; + private String streamName; + + @Nullable + private RabbitStreamListenerObservationConvention observationConvention; + /** * Construct an instance using the provided environment. * @param environment the environment. @@ -108,6 +116,7 @@ public synchronized void setQueueNames(String... queueNames) { Assert.isTrue(queueNames != null && queueNames.length == 1, "Only one stream is supported"); this.builder.stream(queueNames[0]); this.simpleStream = true; + this.streamName = queueNames[0]; } /** @@ -139,6 +148,7 @@ public synchronized void superStream(String streamName, String name, int consume .singleActiveConsumer() .name(name); this.superStream = true; + this.streamName = streamName; } /** @@ -171,34 +181,6 @@ public synchronized void setConsumerCustomizer(ConsumerCustomizer consumerCustom this.consumerCustomizer = consumerCustomizer; } - /** - * The 'id' attribute of the listener. - * @return the id (or the container bean name if no id set). - */ - @Nullable - public String getListenerId() { - return this.listenerId != null ? this.listenerId : this.beanName; - } - - @Override - public void setListenerId(String listenerId) { - this.listenerId = listenerId; - } - - /** - * Return the bean name. - * @return the bean name. - */ - @Nullable - public String getBeanName() { - return this.beanName; - } - - @Override - public void setBeanName(String beanName) { - this.beanName = beanName; - } - @Override public void setAutoStartup(boolean autoStart) { this.autoStartup = autoStart; @@ -226,6 +208,22 @@ public Object getMessageListener() { return this.messageListener; } + /** + * Set a RabbitStreamListenerObservationConvention; used to add additional key/values + * to observations when using a {@link StreamMessageListener}. + * @param observationConvention the convention. + * @since 3.0.5 + */ + public void setObservationConvention(RabbitStreamListenerObservationConvention observationConvention) { + this.observationConvention = observationConvention; + } + + @Override + public void afterPropertiesSet() { + checkMicrometer(); + checkObservation(); + } + @Override public synchronized boolean isRunning() { return this.consumers.size() > 0; @@ -263,21 +261,72 @@ public synchronized void stop() { public void setupMessageListener(MessageListener messageListener) { adviseIfNeeded(messageListener); this.builder.messageHandler((context, message) -> { + ObservationRegistry registry = getObservationRegistry(); + Object sample = null; + MicrometerHolder micrometerHolder = getMicrometerHolder(); + if (micrometerHolder != null) { + sample = micrometerHolder.start(); + } + Observation observation = + RabbitStreamListenerObservation.STREAM_LISTENER_OBSERVATION.observation(this.observationConvention, + DefaultRabbitStreamListenerObservationConvention.INSTANCE, + () -> new RabbitStreamMessageReceiverContext(message, getListenerId(), this.streamName), + registry); + Object finalSample = sample; if (this.streamListener != null) { - this.streamListener.onStreamMessage(message, context); + observation.observe(() -> { + try { + this.streamListener.onStreamMessage(message, context); + if (finalSample != null) { + micrometerHolder.success(finalSample, this.streamName); + } + } + catch (RuntimeException rtex) { + if (finalSample != null) { + micrometerHolder.failure(finalSample, this.streamName, rtex.getClass().getSimpleName()); + } + throw rtex; + } + catch (Exception ex) { + if (finalSample != null) { + micrometerHolder.failure(finalSample, this.streamName, ex.getClass().getSimpleName()); + } + throw RabbitExceptionTranslator.convertRabbitAccessException(ex); + } + }); } else { Message message2 = this.streamConverter.toMessage(message, new StreamMessageProperties(context)); if (this.messageListener instanceof ChannelAwareMessageListener) { try { - ((ChannelAwareMessageListener) this.messageListener).onMessage(message2, null); + observation.observe(() -> { + try { + ((ChannelAwareMessageListener) this.messageListener).onMessage(message2, null); + if (finalSample != null) { + micrometerHolder.success(finalSample, this.streamName); + } + } + catch (RuntimeException rtex) { + if (finalSample != null) { + micrometerHolder.failure(finalSample, this.streamName, + rtex.getClass().getSimpleName()); + } + throw rtex; + } + catch (Exception ex) { + if (finalSample != null) { + micrometerHolder.failure(finalSample, this.streamName, ex.getClass().getSimpleName()); + } + throw RabbitExceptionTranslator.convertRabbitAccessException(ex); + } + }); } catch (Exception ex) { // NOSONAR - this.logger.error(ex, "Listner threw an exception"); + this.logger.error(ex, "Listener threw an exception"); } } else { - this.messageListener.onMessage(message2); + observation.observe(() -> this.messageListener.onMessage(message2)); } } }); diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamListenerObservation.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamListenerObservation.java new file mode 100644 index 0000000000..cc12130fdf --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamListenerObservation.java @@ -0,0 +1,101 @@ +/* + * Copyright 2023 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.rabbit.stream.micrometer; + +import io.micrometer.common.KeyValues; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.docs.ObservationDocumentation; + +/** + * Spring Rabbit Observation for stream listeners. + * + * @author Gary Russell + * @since 3.0.5 + * + */ +public enum RabbitStreamListenerObservation implements ObservationDocumentation { + + /** + * Observation for Rabbit stream listeners. + */ + STREAM_LISTENER_OBSERVATION { + + + @Override + public Class> getDefaultConvention() { + return DefaultRabbitStreamListenerObservationConvention.class; + } + + @Override + public String getPrefix() { + return "spring.rabbit.stream.listener"; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return ListenerLowCardinalityTags.values(); + } + + }; + + /** + * Low cardinality tags. + */ + public enum ListenerLowCardinalityTags implements KeyName { + + /** + * Listener id. + */ + LISTENER_ID { + + @Override + public String asString() { + return "spring.rabbit.stream.listener.id"; + } + + } + + } + + /** + * Default {@link RabbitStreamListenerObservationConvention} for Rabbit listener key values. + */ + public static class DefaultRabbitStreamListenerObservationConvention + implements RabbitStreamListenerObservationConvention { + + /** + * A singleton instance of the convention. + */ + public static final DefaultRabbitStreamListenerObservationConvention INSTANCE = + new DefaultRabbitStreamListenerObservationConvention(); + + @Override + public KeyValues getLowCardinalityKeyValues(RabbitStreamMessageReceiverContext context) { + return KeyValues.of(RabbitStreamListenerObservation.ListenerLowCardinalityTags.LISTENER_ID.asString(), + context.getListenerId()); + } + + @Override + public String getContextualName(RabbitStreamMessageReceiverContext context) { + return context.getSource() + " receive"; + } + + } + +} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamListenerObservationConvention.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamListenerObservationConvention.java new file mode 100644 index 0000000000..ffa90c5d23 --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamListenerObservationConvention.java @@ -0,0 +1,42 @@ +/* + * Copyright 2023 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.rabbit.stream.micrometer; + +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationConvention; + +/** + * {@link ObservationConvention} for Rabbit stream listener key values. + * + * @author Gary Russell + * @since 3.0.5 + * + */ +public interface RabbitStreamListenerObservationConvention + extends ObservationConvention { + + @Override + default boolean supportsContext(Context context) { + return context instanceof RabbitStreamMessageReceiverContext; + } + + @Override + default String getName() { + return "spring.rabbit.stream.listener"; + } + +} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamMessageReceiverContext.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamMessageReceiverContext.java new file mode 100644 index 0000000000..8cc33b7bc5 --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamMessageReceiverContext.java @@ -0,0 +1,102 @@ +/* + * Copyright 2023 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.rabbit.stream.micrometer; + +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import org.springframework.amqp.rabbit.support.micrometer.RabbitListenerObservation; +import org.springframework.amqp.rabbit.support.micrometer.RabbitListenerObservationConvention; +import org.springframework.amqp.rabbit.support.micrometer.RabbitMessageReceiverContext; + +import com.rabbitmq.stream.Message; +import io.micrometer.common.KeyValues; +import io.micrometer.observation.transport.ReceiverContext; + +/** + * {@link ReceiverContext} for stream {@link Message}s. + * + * @author Gary Russell + * @since 3.0.5 + * + */ +public class RabbitStreamMessageReceiverContext extends ReceiverContext { + + private final String listenerId; + + private final Message message; + + private final String stream; + + public RabbitStreamMessageReceiverContext(Message message, String listenerId, String stream) { + super((carrier, key) -> { + Map props = carrier.getApplicationProperties(); + if (props != null) { + Object value = carrier.getApplicationProperties().get(key); + if (value instanceof String string) { + return string; + } + else if (value instanceof byte[] bytes) { + return new String(bytes, StandardCharsets.UTF_8); + } + } + return null; + }); + setCarrier(message); + this.message = message; + this.listenerId = listenerId; + this.stream = stream; + setRemoteServiceName("RabbitMQ Stream"); + } + + public String getListenerId() { + return this.listenerId; + } + + /** + * Return the source (stream) for this message. + * @return the source. + */ + public String getSource() { + return this.stream; + } + + /** + * Default {@link RabbitListenerObservationConvention} for Rabbit listener key values. + */ + public static class DefaultRabbitListenerObservationConvention implements RabbitListenerObservationConvention { + + /** + * A singleton instance of the convention. + */ + public static final DefaultRabbitListenerObservationConvention INSTANCE = + new DefaultRabbitListenerObservationConvention(); + + @Override + public KeyValues getLowCardinalityKeyValues(RabbitMessageReceiverContext context) { + return KeyValues.of(RabbitListenerObservation.ListenerLowCardinalityTags.LISTENER_ID.asString(), + context.getListenerId()); + } + + @Override + public String getContextualName(RabbitMessageReceiverContext context) { + return context.getSource() + " receive"; + } + + } + +} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamMessageSenderContext.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamMessageSenderContext.java new file mode 100644 index 0000000000..d8afcb264f --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamMessageSenderContext.java @@ -0,0 +1,62 @@ +/* + * Copyright 2023 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.rabbit.stream.micrometer; + +import java.util.Map; + +import com.rabbitmq.stream.Message; +import io.micrometer.observation.transport.SenderContext; + +/** + * {@link SenderContext} for {@link Message}s. + * + * @author Gary Russell + * @since 3.0.5 + * + */ +public class RabbitStreamMessageSenderContext extends SenderContext { + + private final String beanName; + + private final String destination; + + public RabbitStreamMessageSenderContext(Message message, String beanName, String destination) { + super((carrier, key, value) -> { + Map props = message.getApplicationProperties(); + if (props != null) { + props.put(key, value); + } + }); + setCarrier(message); + this.beanName = beanName; + this.destination = destination; + setRemoteServiceName("RabbitMQ Stream"); + } + + public String getBeanName() { + return this.beanName; + } + + /** + * Return the destination - {@code exchange/routingKey}. + * @return the destination. + */ + public String getDestination() { + return this.destination; + } + +} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamTemplateObservation.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamTemplateObservation.java new file mode 100644 index 0000000000..cd095f0617 --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamTemplateObservation.java @@ -0,0 +1,103 @@ +/* + * Copyright 2023 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.rabbit.stream.micrometer; + +import org.springframework.rabbit.stream.producer.RabbitStreamTemplate; + +import io.micrometer.common.KeyValues; +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.docs.ObservationDocumentation; + +/** + * Spring RabbitMQ Observation for + * {@link org.springframework.rabbit.stream.producer.RabbitStreamTemplate}. + * + * @author Gary Russell + * @since 3.0.5 + * + */ +public enum RabbitStreamTemplateObservation implements ObservationDocumentation { + + /** + * Observation for {@link RabbitStreamTemplate}s. + */ + STREAM_TEMPLATE_OBSERVATION { + + @Override + public Class> getDefaultConvention() { + return DefaultRabbitStreamTemplateObservationConvention.class; + } + + @Override + public String getPrefix() { + return "spring.rabbit.stream.template"; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return TemplateLowCardinalityTags.values(); + } + + }; + + /** + * Low cardinality tags. + */ + public enum TemplateLowCardinalityTags implements KeyName { + + /** + * Bean name of the template. + */ + BEAN_NAME { + + @Override + public String asString() { + return "spring.rabbit.stream.template.name"; + } + + } + + } + + /** + * Default {@link RabbitStreamTemplateObservationConvention} for Rabbit template key values. + */ + public static class DefaultRabbitStreamTemplateObservationConvention + implements RabbitStreamTemplateObservationConvention { + + /** + * A singleton instance of the convention. + */ + public static final DefaultRabbitStreamTemplateObservationConvention INSTANCE = + new DefaultRabbitStreamTemplateObservationConvention(); + + @Override + public KeyValues getLowCardinalityKeyValues(RabbitStreamMessageSenderContext context) { + return KeyValues.of(RabbitStreamTemplateObservation.TemplateLowCardinalityTags.BEAN_NAME.asString(), + context.getBeanName()); + } + + @Override + public String getContextualName(RabbitStreamMessageSenderContext context) { + return context.getDestination() + " send"; + } + + } + +} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamTemplateObservationConvention.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamTemplateObservationConvention.java new file mode 100644 index 0000000000..1989bd079e --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamTemplateObservationConvention.java @@ -0,0 +1,42 @@ +/* + * Copyright 2023 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.rabbit.stream.micrometer; + +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationConvention; + +/** + * {@link ObservationConvention} for Rabbit stream template key values. + * + * @author Gary Russell + * @since 3.0.5 + * + */ +public interface RabbitStreamTemplateObservationConvention + extends ObservationConvention { + + @Override + default boolean supportsContext(Context context) { + return context instanceof RabbitStreamMessageSenderContext; + } + + @Override + default String getName() { + return "spring.rabbit.stream.template"; + } + +} diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/package-info.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/package-info.java new file mode 100644 index 0000000000..14fb3141b3 --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/package-info.java @@ -0,0 +1,6 @@ +/** + * Provides classes for Micrometer support. + */ +@org.springframework.lang.NonNullApi +@org.springframework.lang.NonNullFields +package org.springframework.rabbit.stream.micrometer; diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java index 2c7c71bf60..5a8c635748 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 the original author or authors. + * Copyright 2021-2023 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. @@ -23,9 +23,17 @@ import org.springframework.amqp.core.MessagePostProcessor; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.amqp.support.converter.SimpleMessageConverter; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.core.log.LogAccessor; import org.springframework.lang.Nullable; +import org.springframework.rabbit.stream.micrometer.RabbitStreamMessageSenderContext; +import org.springframework.rabbit.stream.micrometer.RabbitStreamTemplateObservation; +import org.springframework.rabbit.stream.micrometer.RabbitStreamTemplateObservation.DefaultRabbitStreamTemplateObservationConvention; +import org.springframework.rabbit.stream.micrometer.RabbitStreamTemplateObservationConvention; import org.springframework.rabbit.stream.support.StreamMessageProperties; import org.springframework.rabbit.stream.support.converter.DefaultStreamMessageConverter; import org.springframework.rabbit.stream.support.converter.StreamMessageConverter; @@ -37,6 +45,8 @@ import com.rabbitmq.stream.MessageBuilder; import com.rabbitmq.stream.Producer; import com.rabbitmq.stream.ProducerBuilder; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; /** * Default implementation of {@link RabbitStreamOperations}. @@ -45,10 +55,12 @@ * @since 2.4 * */ -public class RabbitStreamTemplate implements RabbitStreamOperations, BeanNameAware { +public class RabbitStreamTemplate implements RabbitStreamOperations, ApplicationContextAware, BeanNameAware { protected final LogAccessor logger = new LogAccessor(getClass()); // NOSONAR + private ApplicationContext applicationContext; + private final Environment environment; private final String streamName; @@ -67,6 +79,15 @@ public class RabbitStreamTemplate implements RabbitStreamOperations, BeanNameAwa private ProducerCustomizer producerCustomizer = (name, builder) -> { }; + private boolean observationEnabled; + + @Nullable + private RabbitStreamTemplateObservationConvention observationConvention; + + private volatile boolean observationRegistryObtained; + + private ObservationRegistry observationRegistry; + /** * Construct an instance with the provided {@link Environment}. * @param environment the environment. @@ -100,6 +121,11 @@ private synchronized Producer createOrGetProducer() { return this.producer; } + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + @Override public synchronized void setBeanName(String name) { this.beanName = name; @@ -144,6 +170,16 @@ public synchronized void setProducerCustomizer(ProducerCustomizer producerCustom this.producerCustomizer = producerCustomizer; } + /** + * Set to true to enable Micrometer observation. + * @param observationEnabled true to enable. + * @since 3.0.5 + */ + public void setObservationEnabled(boolean observationEnabled) { + this.observationEnabled = observationEnabled; + } + + @Override public MessageConverter messageConverter() { return this.messageConverter; @@ -159,7 +195,7 @@ public StreamMessageConverter streamMessageConverter() { @Override public CompletableFuture send(Message message) { CompletableFuture future = new CompletableFuture<>(); - createOrGetProducer().send(this.streamConverter.fromMessage(message), handleConfirm(future)); + observeSend(this.streamConverter.fromMessage(message), future); return future; } @@ -188,19 +224,49 @@ public CompletableFuture convertAndSend(Object message, @Nullable Messa @Override public CompletableFuture send(com.rabbitmq.stream.Message message) { CompletableFuture future = new CompletableFuture<>(); - createOrGetProducer().send(message, handleConfirm(future)); + observeSend(message, future); return future; } + private void observeSend(com.rabbitmq.stream.Message message, CompletableFuture future) { + Observation observation = RabbitStreamTemplateObservation.STREAM_TEMPLATE_OBSERVATION.observation( + this.observationConvention, DefaultRabbitStreamTemplateObservationConvention.INSTANCE, + () -> new RabbitStreamMessageSenderContext(message, this.beanName, this.streamName), + obtainObservationRegistry()); + observation.start(); + try { + createOrGetProducer().send(message, handleConfirm(future, observation)); + } + catch (Exception ex) { + observation.error(ex); + observation.stop(); + future.completeExceptionally(ex); + } + } + + @Nullable + private ObservationRegistry obtainObservationRegistry() { + if (!this.observationRegistryObtained && this.observationEnabled) { + if (this.applicationContext != null) { + ObjectProvider registry = + this.applicationContext.getBeanProvider(ObservationRegistry.class); + this.observationRegistry = registry.getIfUnique(); + } + this.observationRegistryObtained = true; + } + return this.observationRegistry; + } + @Override public MessageBuilder messageBuilder() { return createOrGetProducer().messageBuilder(); } - private ConfirmationHandler handleConfirm(CompletableFuture future) { + private ConfirmationHandler handleConfirm(CompletableFuture future, Observation observation) { return confStatus -> { if (confStatus.isConfirmed()) { future.complete(true); + observation.stop(); } else { int code = confStatus.getCode(); @@ -222,7 +288,10 @@ private ConfirmationHandler handleConfirm(CompletableFuture future) { errorMessage = "Unknown code: " + code; break; } - future.completeExceptionally(new StreamSendException(errorMessage, code)); + StreamSendException ex = new StreamSendException(errorMessage, code); + observation.error(ex); + observation.stop(); + future.completeExceptionally(ex); } }; } diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverter.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverter.java index a49bf67ad4..ac5664b059 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverter.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverter.java @@ -104,8 +104,8 @@ public com.rabbitmq.stream.Message fromMessage(Message message) throws MessageCo .acceptIfNotNull(mProps.getGroupId(), propsBuilder::groupId) .acceptIfNotNull(mProps.getGroupSequence(), propsBuilder::groupSequence) .acceptIfNotNull(mProps.getReplyToGroupId(), propsBuilder::replyToGroupId); + ApplicationPropertiesBuilder appPropsBuilder = builder.applicationProperties(); if (mProps.getHeaders().size() > 0) { - ApplicationPropertiesBuilder appPropsBuilder = builder.applicationProperties(); mProps.getHeaders().forEach((key, val) -> { mapProp(key, val, appPropsBuilder); }); diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java index d15fbdb164..f1d847d849 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java @@ -66,6 +66,12 @@ import com.rabbitmq.stream.Message; import com.rabbitmq.stream.MessageHandler.Context; import com.rabbitmq.stream.OffsetSpecification; +import io.micrometer.common.KeyValues; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.core.tck.MeterRegistryAssert; +import io.micrometer.observation.ObservationRegistry; /** * @author Gary Russell @@ -80,7 +86,7 @@ public class RabbitListenerTests extends AbstractTestContainerTests { Config config; @Test - void simple(@Autowired RabbitStreamTemplate template) throws Exception { + void simple(@Autowired RabbitStreamTemplate template, @Autowired MeterRegistry meterRegistry) throws Exception { Future future = template.convertAndSend("foo"); assertThat(future.get(10, TimeUnit.SECONDS)).isTrue(); future = template.convertAndSend("bar", msg -> msg); @@ -95,16 +101,35 @@ void simple(@Autowired RabbitStreamTemplate template) throws Exception { assertThat(this.config.latch1.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(this.config.received).containsExactly("foo", "foo", "bar", "baz", "qux"); assertThat(this.config.id).isEqualTo("testNative"); + MeterRegistryAssert.assertThat(meterRegistry) + .hasTimerWithNameAndTags("spring.rabbit.stream.template", + KeyValues.of("spring.rabbit.stream.template.name", "streamTemplate1")) + .hasTimerWithNameAndTags("spring.rabbit.stream.listener", + KeyValues.of("spring.rabbit.stream.listener.id", "obs")) + .hasTimerWithNameAndTags("spring.rabbitmq.listener", + KeyValues.of("listener.id", "notObs") + .and("queue", "test.stream.queue1")); } @Test - void nativeMsg(@Autowired RabbitTemplate template) throws InterruptedException { + void nativeMsg(@Autowired RabbitTemplate template, @Autowired MeterRegistry meterRegistry) + throws InterruptedException { + template.convertAndSend("test.stream.queue2", "foo"); + // Send a second to ensure the timer exists before the assertion + template.convertAndSend("test.stream.queue2", "bar"); assertThat(this.config.latch2.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(this.config.receivedNative).isNotNull(); assertThat(this.config.context).isNotNull(); assertThat(this.config.latch3.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(this.config.latch4.await(10, TimeUnit.SECONDS)).isTrue(); + MeterRegistryAssert.assertThat(meterRegistry) + .hasTimerWithNameAndTags("spring.rabbit.stream.listener", + KeyValues.of("spring.rabbit.stream.listener.id", "testObsNative")) + .hasTimerWithNameAndTags("spring.rabbitmq.listener", + KeyValues.of("listener.id", "testNative")) + .hasTimerWithNameAndTags("spring.rabbitmq.listener", + KeyValues.of("listener.id", "testNativeFail")); } @SuppressWarnings("unchecked") @@ -145,11 +170,11 @@ private WebClient createClient(String adminUser, String adminPassword) { @EnableRabbit public static class Config { - final CountDownLatch latch1 = new CountDownLatch(5); + final CountDownLatch latch1 = new CountDownLatch(9); - final CountDownLatch latch2 = new CountDownLatch(1); + final CountDownLatch latch2 = new CountDownLatch(4); - final CountDownLatch latch3 = new CountDownLatch(3); + final CountDownLatch latch3 = new CountDownLatch(6); final CountDownLatch latch4 = new CountDownLatch(1); @@ -163,6 +188,18 @@ public static class Config { volatile String id; + @Bean + MeterRegistry meterReg() { + return new SimpleMeterRegistry(); + } + + @Bean + ObservationRegistry obsReg(MeterRegistry meterRegistry) { + ObservationRegistry registry = ObservationRegistry.create(); + registry.observationConfig().observationHandler(new DefaultMeterObservationHandler(meterRegistry)); + return registry; + } + @Bean static Environment environment() { return Environment.builder() @@ -227,7 +264,6 @@ public int getPhase() { return 0; } - }; } @@ -238,7 +274,19 @@ RabbitListenerContainerFactory rabbitListenerContainerF return factory; } - @RabbitListener(queues = "test.stream.queue1") + @Bean + RabbitListenerContainerFactory observableFactory(Environment env) { + StreamRabbitListenerContainerFactory factory = new StreamRabbitListenerContainerFactory(env); + factory.setObservationEnabled(true); + factory.setConsumerCustomizer((id, builder) -> { + builder.name(id) + .offset(OffsetSpecification.first()) + .manualTrackingStrategy(); + }); + return factory; + } + + @RabbitListener(id = "notObs", queues = "test.stream.queue1") void listen(String in) { this.received.add(in); this.latch1.countDown(); @@ -247,6 +295,11 @@ void listen(String in) { } } + @RabbitListener(id = "obs", queues = "test.stream.queue1", containerFactory = "observableFactory") + void listenObs(String in) { + this.latch1.countDown(); + } + @Bean public StreamRetryOperationsInterceptorFactoryBean sfb() { StreamRetryOperationsInterceptorFactoryBean rfb = new StreamRetryOperationsInterceptorFactoryBean(); @@ -275,6 +328,21 @@ RabbitListenerContainerFactory nativeFactory(Environmen return factory; } + @Bean + RabbitListenerContainerFactory nativeObsFactory(Environment env, + RetryOperationsInterceptor retry) { + + StreamRabbitListenerContainerFactory factory = new StreamRabbitListenerContainerFactory(env); + factory.setNativeListener(true); + factory.setObservationEnabled(true); + factory.setConsumerCustomizer((id, builder) -> { + builder.name(id) + .offset(OffsetSpecification.first()) + .manualTrackingStrategy(); + }); + return factory; + } + @RabbitListener(id = "testNative", queues = "test.stream.queue2", containerFactory = "nativeFactory") void nativeMsg(Message in, Context context) { this.receivedNative = in; @@ -289,6 +357,14 @@ void nativeMsgFail(Message in, Context context) { throw new RuntimeException("fail all"); } + @RabbitListener(id = "testObsNative", queues = "test.stream.queue2", containerFactory = "nativeObsFactory") + void nativeObsMsg(Message in, Context context) { + this.receivedNative = in; + this.context = context; + this.latch2.countDown(); + context.storeOffset(); + } + @Bean CachingConnectionFactory cf() { return new CachingConnectionFactory("localhost", amqpPort()); @@ -303,6 +379,7 @@ RabbitTemplate template(CachingConnectionFactory cf) { RabbitStreamTemplate streamTemplate1(Environment env) { RabbitStreamTemplate template = new RabbitStreamTemplate(env, "test.stream.queue1"); template.setProducerCustomizer((name, builder) -> builder.name("test")); + template.setObservationEnabled(true); return template; } diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/micrometer/TracingTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/micrometer/TracingTests.java new file mode 100644 index 0000000000..bd93149e61 --- /dev/null +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/micrometer/TracingTests.java @@ -0,0 +1,218 @@ +/* + * Copyright 2023 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.rabbit.stream.micrometer; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.amqp.rabbit.annotation.EnableRabbit; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.junit.AbstractTestContainerTests; +import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory; +import org.springframework.context.SmartLifecycle; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.rabbit.stream.config.StreamRabbitListenerContainerFactory; +import org.springframework.rabbit.stream.listener.StreamListenerContainer; +import org.springframework.rabbit.stream.producer.RabbitStreamTemplate; +import org.springframework.rabbit.stream.support.StreamAdmin; + +import com.rabbitmq.stream.Address; +import com.rabbitmq.stream.Environment; +import com.rabbitmq.stream.Message; +import com.rabbitmq.stream.OffsetSpecification; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.Span.Kind; +import io.micrometer.tracing.exporter.FinishedSpan; +import io.micrometer.tracing.test.SampleTestRunner; +import io.micrometer.tracing.test.simple.SpanAssert; +import io.micrometer.tracing.test.simple.SpansAssert; + +/** + * @author Gary Russell + * @since 3.0.5 + * + */ +@Testcontainers(disabledWithoutDocker = true) +public class TracingTests extends SampleTestRunner { + + private static final AbstractTestContainerTests atct = new AbstractTestContainerTests() { + }; + + @Override + public SampleTestRunnerConsumer yourCode() throws Exception { + return (bb, meterRegistry) -> { + ObservationRegistry observationRegistry = getObservationRegistry(); + try (AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext()) { + applicationContext.getBeanFactory().registerSingleton("obsReg", observationRegistry); + applicationContext.register(Config.class); + applicationContext.refresh(); + applicationContext.getBean(RabbitStreamTemplate.class).convertAndSend("test").get(10, TimeUnit.SECONDS); + assertThat(applicationContext.getBean(Listener.class).latch1.await(10, TimeUnit.SECONDS)).isTrue(); + } + + List finishedSpans = bb.getFinishedSpans(); + SpansAssert.assertThat(finishedSpans) + .haveSameTraceId() + .hasSize(3); + List producerSpans = finishedSpans.stream() + .filter(span -> span.getKind().equals(Kind.PRODUCER)) + .collect(Collectors.toList()); + List consumerSpans = finishedSpans.stream() + .filter(span -> span.getKind().equals(Kind.CONSUMER)) + .collect(Collectors.toList()); + SpanAssert.assertThat(producerSpans.get(0)) + .hasTag("spring.rabbit.stream.template.name", "streamTemplate1"); + SpanAssert.assertThat(producerSpans.get(0)) + .hasRemoteServiceNameEqualTo("RabbitMQ Stream"); + SpanAssert.assertThat(consumerSpans.get(0)) + .hasTagWithKey("spring.rabbit.stream.listener.id"); + SpanAssert.assertThat(consumerSpans.get(0)) + .hasRemoteServiceNameEqualTo("RabbitMQ Stream"); + assertThat(consumerSpans.get(0).getTags().get("spring.rabbit.stream.listener.id")).isIn("one", "two"); + SpanAssert.assertThat(consumerSpans.get(1)) + .hasTagWithKey("spring.rabbit.stream.listener.id"); + assertThat(consumerSpans.get(1).getTags().get("spring.rabbit.stream.listener.id")).isIn("one", "two"); + assertThat(consumerSpans.get(0).getTags().get("spring.rabbit.stream.listener.id")) + .isNotEqualTo(consumerSpans.get(1).getTags().get("spring.rabbit.stream.listener.id")); + }; + } + + @EnableRabbit + @Configuration(proxyBeanMethods = false) + public static class Config { + + @Bean + static Environment environment() { + return Environment.builder() + .addressResolver(add -> new Address("localhost", AbstractTestContainerTests.streamPort())) + .build(); + } + + @Bean + StreamAdmin streamAdmin(Environment env) { + StreamAdmin streamAdmin = new StreamAdmin(env, sc -> { + sc.stream("trace.stream.queue1").create(); + }); + streamAdmin.setAutoStartup(false); + return streamAdmin; + } + + @Bean + SmartLifecycle creator(Environment env, StreamAdmin admin) { + return new SmartLifecycle() { + + boolean running; + + @Override + public void stop() { + clean(env); + this.running = false; + } + + @Override + public void start() { + clean(env); + admin.start(); + this.running = true; + } + + private void clean(Environment env) { + try { + env.deleteStream("trace.stream.queue1"); + } + catch (Exception e) { + } + } + + @Override + public boolean isRunning() { + return this.running; + } + + @Override + public int getPhase() { + return 0; + } + + }; + } + + @Bean + RabbitListenerContainerFactory rabbitListenerContainerFactory(Environment env) { + StreamRabbitListenerContainerFactory factory = new StreamRabbitListenerContainerFactory(env); + factory.setObservationEnabled(true); + factory.setConsumerCustomizer((id, builder) -> { + builder.name(id) + .offset(OffsetSpecification.first()) + .manualTrackingStrategy(); + }); + return factory; + } + + @Bean + RabbitListenerContainerFactory nativeFactory(Environment env) { + StreamRabbitListenerContainerFactory factory = new StreamRabbitListenerContainerFactory(env); + factory.setNativeListener(true); + factory.setObservationEnabled(true); + factory.setConsumerCustomizer((id, builder) -> { + builder.name(id) + .offset(OffsetSpecification.first()) + .manualTrackingStrategy(); + }); + return factory; + } + + @Bean + RabbitStreamTemplate streamTemplate1(Environment env) { + RabbitStreamTemplate template = new RabbitStreamTemplate(env, "trace.stream.queue1"); + template.setProducerCustomizer((name, builder) -> builder.name("test")); + template.setObservationEnabled(true); + return template; + } + + @Bean + Listener listener() { + return new Listener(); + } + + } + + public static class Listener { + + CountDownLatch latch1 = new CountDownLatch(2); + + @RabbitListener(id = "one", queues = "trace.stream.queue1") + void listen(String in) { + latch1.countDown(); + } + + @RabbitListener(id = "two", queues = "trace.stream.queue1", containerFactory = "nativeFactory") + public void listen(Message in) { + latch1.countDown(); + } + + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java index 2f38812a35..fbeff0bb1b 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2023 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. @@ -36,8 +36,6 @@ import org.springframework.amqp.support.ConsumerTagStrategy; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.amqp.utils.JavaUtils; -import org.springframework.beans.BeansException; -import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; @@ -101,8 +99,6 @@ public abstract class AbstractRabbitListenerContainerFactory - implements RabbitListenerContainerFactory { + implements RabbitListenerContainerFactory, ApplicationContextAware { private Boolean defaultRequeueRejected; @@ -58,6 +61,12 @@ public abstract class BaseRabbitListenerContainerFactory replyPostProcessorProvider; + private Boolean micrometerEnabled; + + private Boolean observationEnabled; + + private ApplicationContext applicationContext; + @Override public abstract C createListenerContainer(RabbitListenerEndpoint endpoint); @@ -171,4 +180,43 @@ public void setAdviceChain(Advice... adviceChain) { this.adviceChain = adviceChain == null ? null : Arrays.copyOf(adviceChain, adviceChain.length); } + /** + * Set to false to disable micrometer listener timers. When true, ignored + * if {@link #setObservationEnabled(boolean)} is set to true. + * @param micrometerEnabled false to disable. + * @since 3.0 + * @see #setObservationEnabled(boolean) + */ + public void setMicrometerEnabled(boolean micrometerEnabled) { + this.micrometerEnabled = micrometerEnabled; + } + + protected Boolean getMicrometerEnabled() { + return this.micrometerEnabled; + } + + /** + * Enable observation via micrometer; disables basic Micrometer timers enabled + * by {@link #setMicrometerEnabled(boolean)}. + * @param observationEnabled true to enable. + * @since 3.0 + * @see #setMicrometerEnabled(boolean) + */ + public void setObservationEnabled(boolean observationEnabled) { + this.observationEnabled = observationEnabled; + } + + protected Boolean getObservationEnabled() { + return this.observationEnabled; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + protected ApplicationContext getApplicationContext() { + return this.applicationContext; + } + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java index 9cacab8731..0b4ebe5ff3 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index 7bf4483ddd..88ef0ef456 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -52,7 +52,6 @@ import org.springframework.amqp.rabbit.connection.Connection; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactoryUtils; -import org.springframework.amqp.rabbit.connection.RabbitAccessor; import org.springframework.amqp.rabbit.connection.RabbitResourceHolder; import org.springframework.amqp.rabbit.connection.RabbitUtils; import org.springframework.amqp.rabbit.connection.RoutingConnectionFactory; @@ -76,10 +75,7 @@ import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactoryUtils; -import org.springframework.beans.factory.BeanNameAware; -import org.springframework.beans.factory.DisposableBean; import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.core.task.SimpleAsyncTaskExecutor; @@ -89,7 +85,6 @@ import org.springframework.transaction.interceptor.TransactionAttribute; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; import org.springframework.util.ErrorHandler; import org.springframework.util.StringUtils; import org.springframework.util.backoff.BackOff; @@ -113,9 +108,8 @@ * @author Mohammad Hewedy * @author Mat Jaggard */ -public abstract class AbstractMessageListenerContainer extends RabbitAccessor - implements MessageListenerContainer, ApplicationContextAware, BeanNameAware, DisposableBean, - ApplicationEventPublisherAware { +public abstract class AbstractMessageListenerContainer extends ObservableListenerContainer + implements ApplicationEventPublisherAware { private static final int EXIT_99 = 99; @@ -134,9 +128,6 @@ public abstract class AbstractMessageListenerContainer extends RabbitAccessor public static final long DEFAULT_SHUTDOWN_TIMEOUT = 5000; - private static final boolean MICROMETER_PRESENT = ClassUtils.isPresent( - "io.micrometer.core.instrument.MeterRegistry", AbstractMessageListenerContainer.class.getClassLoader()); - private final Object lifecycleMonitor = new Object(); private final ContainerDelegate delegate = this::actualInvokeListener; @@ -145,8 +136,6 @@ public abstract class AbstractMessageListenerContainer extends RabbitAccessor private final Map consumerArgs = new HashMap<>(); - private final Map micrometerTags = new HashMap<>(); - private ContainerDelegate proxy = this.delegate; private final AtomicBoolean logDeclarationException = new AtomicBoolean(true); @@ -160,8 +149,6 @@ public abstract class AbstractMessageListenerContainer extends RabbitAccessor private TransactionAttribute transactionAttribute = new DefaultTransactionAttribute(); - private String beanName = "not.a.Spring.bean"; - private Executor taskExecutor = new SimpleAsyncTaskExecutor(); private boolean taskExecutorSet; @@ -208,10 +195,6 @@ public abstract class AbstractMessageListenerContainer extends RabbitAccessor private Collection afterReceivePostProcessors; - private ApplicationContext applicationContext; - - private String listenerId; - private Advice[] adviceChain = new Advice[0]; @Nullable @@ -245,12 +228,6 @@ public abstract class AbstractMessageListenerContainer extends RabbitAccessor private BatchingStrategy batchingStrategy = new SimpleBatchingStrategy(0, 0, 0L); - private MicrometerHolder micrometerHolder; - - private boolean micrometerEnabled = true; - - private boolean observationEnabled = false; - private boolean isBatchListener; private long consumeDelay; @@ -605,29 +582,6 @@ public int getPhase() { return this.phase; } - @Override - public void setBeanName(String beanName) { - this.beanName = beanName; - } - - /** - * @return The bean name that this listener container has been assigned in its containing bean factory, if any. - */ - @Nullable - protected final String getBeanName() { - return this.beanName; - } - - @Nullable - protected final ApplicationContext getApplicationContext() { - return this.applicationContext; - } - - @Override - public final void setApplicationContext(ApplicationContext applicationContext) { - this.applicationContext = applicationContext; - } - @Override public ConnectionFactory getConnectionFactory() { ConnectionFactory connectionFactory = super.getConnectionFactory(); @@ -703,19 +657,6 @@ protected RoutingConnectionFactory getRoutingConnectionFactory() { return super.getConnectionFactory() instanceof RoutingConnectionFactory rcf ? rcf : null; } - /** - * The 'id' attribute of the listener. - * @return the id (or the container bean name if no id set). - */ - public String getListenerId() { - return this.listenerId != null ? this.listenerId : this.beanName; - } - - @Override - public void setListenerId(String listenerId) { - this.listenerId = listenerId; - } - /** * Set the implementation of {@link ConsumerTagStrategy} to generate consumer tags. * By default, the RabbitMQ server generates consumer tags. @@ -1151,39 +1092,6 @@ protected Collection getAfterReceivePostProcessors() { return this.afterReceivePostProcessors; } - /** - * Set additional tags for the Micrometer listener timers. - * @param tags the tags. - * @since 2.2 - */ - public void setMicrometerTags(Map tags) { - if (tags != null) { - this.micrometerTags.putAll(tags); - } - } - - /** - * Set to false to disable micrometer listener timers. When true, ignored - * if {@link #setObservationEnabled(boolean)} is set to true. - * @param micrometerEnabled false to disable. - * @since 2.2 - * @see #setObservationEnabled(boolean) - */ - public void setMicrometerEnabled(boolean micrometerEnabled) { - this.micrometerEnabled = micrometerEnabled; - } - - /** - * Enable observation via micrometer; disables basic Micrometer timers enabled - * by {@link #setMicrometerEnabled(boolean)}. - * @param observationEnabled true to enable. - * @since 3.0 - * @see #setMicrometerEnabled(boolean) - */ - public void setObservationEnabled(boolean observationEnabled) { - this.observationEnabled = observationEnabled; - } - /** * Set an observation convention; used to add additional key/values to observations. * @param observationConvention the convention. @@ -1263,16 +1171,7 @@ public void afterPropertiesSet() { "channelTransacted=false"); validateConfiguration(); initialize(); - try { - if (this.micrometerHolder == null && MICROMETER_PRESENT && this.micrometerEnabled && !this.observationEnabled - && this.applicationContext != null) { - this.micrometerHolder = new MicrometerHolder(this.applicationContext, getListenerId(), - this.micrometerTags); - } - } - catch (IllegalStateException e) { - this.logger.debug("Could not enable micrometer timers", e); - } + checkMicrometer(); if (this.isAsyncReplies() && !AcknowledgeMode.MANUAL.equals(this.acknowledgeMode)) { this.acknowledgeMode = AcknowledgeMode.MANUAL; } @@ -1311,9 +1210,7 @@ protected void initializeProxy(Object delegate) { @Override public void destroy() { shutdown(); - if (this.micrometerHolder != null) { - this.micrometerHolder.destroy(); - } + super.destroy(); } // ------------------------------------------------------------------------- @@ -1432,9 +1329,7 @@ public void start() { } } } - if (this.observationEnabled) { - obtainObservationRegistry(this.applicationContext); - } + checkObservation(); try { logger.debug("Starting Rabbit listener container."); configureAdminIfNeeded(); @@ -1556,20 +1451,21 @@ protected void executeListenerAndHandleException(Channel channel, Object data) { throw new MessageRejectedWhileStoppingException(); } Object sample = null; - if (this.micrometerHolder != null) { - sample = this.micrometerHolder.start(); + MicrometerHolder micrometerHolder = getMicrometerHolder(); + if (micrometerHolder != null) { + sample = micrometerHolder.start(); } try { doExecuteListener(channel, data); if (sample != null) { - this.micrometerHolder.success(sample, data instanceof Message message + micrometerHolder.success(sample, data instanceof Message message ? message.getMessageProperties().getConsumerQueue() : queuesAsListString()); } } catch (RuntimeException ex) { if (sample != null) { - this.micrometerHolder.failure(sample, data instanceof Message message + micrometerHolder.failure(sample, data instanceof Message message ? message.getMessageProperties().getConsumerQueue() : queuesAsListString(), ex.getClass().getSimpleName()); } @@ -1866,9 +1762,10 @@ protected void updateLastReceive() { } protected void configureAdminIfNeeded() { - if (this.amqpAdmin == null && this.applicationContext != null) { + ApplicationContext applicationContext = getApplicationContext(); + if (this.amqpAdmin == null && applicationContext != null) { Map admins = - BeanFactoryUtils.beansOfTypeIncludingAncestors(this.applicationContext, AmqpAdmin.class, + BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext, AmqpAdmin.class, false, false); if (admins.size() == 1) { this.amqpAdmin = admins.values().iterator().next(); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MicrometerHolder.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MicrometerHolder.java index 694dabe6e4..72d06e140f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MicrometerHolder.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MicrometerHolder.java @@ -36,7 +36,7 @@ * @since 2.4.6 * */ -final class MicrometerHolder { +public final class MicrometerHolder { private final ConcurrentMap timers = new ConcurrentHashMap<>(); @@ -66,11 +66,11 @@ final class MicrometerHolder { } } - Object start() { + public Object start() { return Timer.start(this.registry); } - void success(Object sample, String queue) { + public void success(Object sample, String queue) { Timer timer = this.timers.get(queue + "none"); if (timer == null) { timer = buildTimer(this.listenerId, "success", queue, "none"); @@ -78,7 +78,7 @@ void success(Object sample, String queue) { ((Sample) sample).stop(timer); } - void failure(Object sample, String queue, String exception) { + public void failure(Object sample, String queue, String exception) { Timer timer = this.timers.get(queue + exception); if (timer == null) { timer = buildTimer(this.listenerId, "failure", queue, exception); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ObservableListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ObservableListenerContainer.java new file mode 100644 index 0000000000..e9a7b7ba32 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ObservableListenerContainer.java @@ -0,0 +1,155 @@ +/* + * Copyright 2023 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; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.amqp.rabbit.connection.RabbitAccessor; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * @author Gary Russell + * @since 3.0.5 + * + */ +public abstract class ObservableListenerContainer extends RabbitAccessor + implements MessageListenerContainer, ApplicationContextAware, BeanNameAware, DisposableBean { + + private static final boolean MICROMETER_PRESENT = ClassUtils.isPresent( + "io.micrometer.core.instrument.MeterRegistry", AbstractMessageListenerContainer.class.getClassLoader()); + + private ApplicationContext applicationContext; + + private final Map micrometerTags = new HashMap<>(); + + private MicrometerHolder micrometerHolder; + + private boolean micrometerEnabled = true; + + private boolean observationEnabled = false; + + private String beanName = "not.a.Spring.bean"; + + private String listenerId; + + @Nullable + protected final ApplicationContext getApplicationContext() { + return this.applicationContext; + } + + @Override + public final void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + protected MicrometerHolder getMicrometerHolder() { + return this.micrometerHolder; + } + + /** + * Set additional tags for the Micrometer listener timers. + * @param tags the tags. + * @since 2.2 + */ + public void setMicrometerTags(Map tags) { + if (tags != null) { + this.micrometerTags.putAll(tags); + } + } + + /** + * Set to false to disable micrometer listener timers. When true, ignored + * if {@link #setObservationEnabled(boolean)} is set to true. + * @param micrometerEnabled false to disable. + * @since 2.2 + * @see #setObservationEnabled(boolean) + */ + public void setMicrometerEnabled(boolean micrometerEnabled) { + this.micrometerEnabled = micrometerEnabled; + } + + /** + * Enable observation via micrometer; disables basic Micrometer timers enabled + * by {@link #setMicrometerEnabled(boolean)}. + * @param observationEnabled true to enable. + * @since 3.0 + * @see #setMicrometerEnabled(boolean) + */ + public void setObservationEnabled(boolean observationEnabled) { + this.observationEnabled = observationEnabled; + } + + protected void checkMicrometer() { + try { + if (this.micrometerHolder == null && MICROMETER_PRESENT && this.micrometerEnabled + && !this.observationEnabled && this.applicationContext != null) { + + this.micrometerHolder = new MicrometerHolder(this.applicationContext, getListenerId(), + this.micrometerTags); + } + } + catch (IllegalStateException e) { + this.logger.debug("Could not enable micrometer timers", e); + } + } + + protected void checkObservation() { + if (this.observationEnabled) { + obtainObservationRegistry(this.applicationContext); + } + } + + @Override + public void setBeanName(String beanName) { + this.beanName = beanName; + } + + /** + * @return The bean name that this listener container has been assigned in its containing bean factory, if any. + */ + @Nullable + protected final String getBeanName() { + return this.beanName; + } + + /** + * The 'id' attribute of the listener. + * @return the id (or the container bean name if no id set). + */ + public String getListenerId() { + return this.listenerId != null ? this.listenerId : this.beanName; + } + + @Override + public void setListenerId(String listenerId) { + this.listenerId = listenerId; + } + + @Override + public void destroy() { + if (this.micrometerHolder != null) { + this.micrometerHolder.destroy(); + } + } + +} diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index 3a8727a337..45c6ee4db3 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -3855,12 +3855,13 @@ Also see <>. Using Micrometer for observation is now supported, since version 3.0, for the `RabbitTemplate` and listener containers. Set `observationEnabled` on each component to enable observation; this will disable <> because the timers will now be managed with each observation. +When using annotated listeners, set `observationEnabled` on the container factory. Refer to https://micrometer.io/docs/tracing[Micrometer Tracing] for more information. To add tags to timers/traces, configure a custom `RabbitTemplateObservationConvention` or `RabbitListenerObservationConvention` to the template or listener container, respectively. -The default implementations add the `bean.name` tag for template observations and `listener.id` tag for containers. +The default implementations add the `name` tag for template observations and `listener.id` tag for containers. You can either subclass `DefaultRabbitTemplateObservationConvention` or `DefaultRabbitListenerObservationConvention` or provide completely new implementations. diff --git a/src/reference/asciidoc/stream.adoc b/src/reference/asciidoc/stream.adoc index 878a036617..ba3e844fdb 100644 --- a/src/reference/asciidoc/stream.adoc +++ b/src/reference/asciidoc/stream.adoc @@ -310,3 +310,22 @@ StreamListenerContainer container(Environment env, String name) { IMPORTANT: At this time, when the concurrency is greater than 1, the actual concurrency is further controlled by the `Environment`; to achieve full concurrency, set the environment's `maxConsumersByConnection` to 1. See https://rabbitmq.github.io/rabbitmq-stream-java-client/snapshot/htmlsingle/#configuring-the-environment[Configuring the Environment]. + +[[stream-micrometer-observation]] +==== Micrometer Observation + +Using Micrometer for observation is now supported, since version 3.0.5, for the `RabbitStreamTemplate` and the stream listener container. +The container now also supports Micrometer timers (when observation is not enabled). + +Set `observationEnabled` on each component to enable observation; this will disable <> because the timers will now be managed with each observation. +When using annotated listeners, set `observationEnabled` on the container factory. + +Refer to https://micrometer.io/docs/tracing[Micrometer Tracing] for more information. + +To add tags to timers/traces, configure a custom `RabbitStreamTemplateObservationConvention` or `RabbitStreamListenerObservationConvention` to the template or listener container, respectively. + +The default implementations add the `name` tag for template observations and `listener.id` tag for containers. + +You can either subclass `DefaultRabbitStreamTemplateObservationConvention` or `DefaultStreamRabbitListenerObservationConvention` or provide completely new implementations. + +See <> for more details. From 0cfe2f94d408001388ddfaf585516c13311562aa Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 15 Jun 2023 12:10:24 -0400 Subject: [PATCH 246/737] GH-2473: Clarify Choosing a Connection Factory (#2474) Resolves https://github.com/spring-projects/spring-amqp/issues/2473 --- src/reference/asciidoc/amqp.adoc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index 45c6ee4db3..c100ea101b 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -213,9 +213,10 @@ There are three connection factories to chose from The first two were added in version 2.3. -For most use cases, the `PooledChannelConnectionFactory` should be used. +For most use cases, the `CachingConnectionFactory` should be used. The `ThreadChannelConnectionFactory` can be used if you want to ensure strict message ordering without the need to use <>. -The `CachingConnectionFactory` should be used if you want to use correlated publisher confirmations or if you wish to open multiple connections, via its `CacheMode`. +The `PooledChannelConnectionFactory` is similar to the `CachingConnectionFactory` in that it uses a single connection and a pool of channels. +It's implementation is simpler but it doesn't support correlated publisher confirmations. Simple publisher confirmations are supported by all three factories. From 469e7f50fe4552f95fc38321bbe52f58bc84f79d Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 15 Jun 2023 13:10:03 -0400 Subject: [PATCH 247/737] GH-2472: Doc SSL with Java or Boot Resolves https://github.com/spring-projects/spring-amqp/issues/2472 --- src/reference/asciidoc/amqp.adoc | 37 ++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index c100ea101b..404c70232e 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -545,7 +545,40 @@ Previously, you had to configure the SSL options programmatically. The following example shows how to configure a `RabbitConnectionFactoryBean`: ==== -[source,xml] +[source,java,role=primary] +.Java +---- +@Bean +RabbitConnectionFactoryBean rabbitConnectionFactory() { + RabbitConnectionFactoryBean factoryBean = new RabbitConnectionFactoryBean(); + factoryBean.setUseSSL(true); + factoryBean.setSslPropertiesLocation(new ClassPathResource("secrets/rabbitSSL.properties")); + return factoryBean; +} + +@Bean +CachingConnectionFactory connectionFactory(ConnectionFactory rabbitConnectionFactory) { + CachingConnectionFactory ccf = new CachingConnectionFactory(rabbitConnectionFactory); + ccf.setHost("..."); + // ... + return ccf; +} +---- +[source,properties,role=secondary] +.Boot application.properties +---- +spring.rabbitmq.ssl.enabled:true +spring.rabbitmq.ssl.keyStore=... +spring.rabbitmq.ssl.keyStoreType=jks +spring.rabbitmq.ssl.keyStorePassword=... +spring.rabbitmq.ssl.trustStore=... +spring.rabbitmq.ssl.trustStoreType=jks +spring.rabbitmq.ssl.trustStorePassword=... +spring.rabbitmq.host=... +... +---- +[source,xml,role=secondary] +.XML ---- - + ---- ==== From 1a53b047cfafe2c2dba182c65a9d9cc847a009cf Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Sat, 17 Jun 2023 09:37:46 -0400 Subject: [PATCH 248/737] Upgrade Spring Framework, Data, Retry, Etc. (#2477) Also Micrometer, Reactor. --- build.gradle | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index 5fa5ce875e..eec11ee2fa 100644 --- a/build.gradle +++ b/build.gradle @@ -60,17 +60,17 @@ ext { log4jVersion = '2.19.0' logbackVersion = '1.4.4' lz4Version = '1.8.0' - micrometerDocsVersion = '1.0.1' - micrometerVersion = '1.10.6' - micrometerTracingVersion = '1.0.4' + micrometerDocsVersion = '1.0.2' + micrometerVersion = '1.10.8' + micrometerTracingVersion = '1.0.7' mockitoVersion = '4.8.1' rabbitmqStreamVersion = '0.8.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.16.0' - reactorVersion = '2022.0.6' + reactorVersion = '2022.0.8' snappyVersion = '1.1.8.4' - springDataVersion = '2022.0.5' - springRetryVersion = '2.0.1' - springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.8' + springDataVersion = '2022.0.7' + springRetryVersion = '2.0.2' + springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.10' testcontainersVersion = '1.17.6' zstdJniVersion = '1.5.0-2' From 7ff0db916304faed1f92e76742753ac2f7c73c2b Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Sat, 17 Jun 2023 14:14:38 +0000 Subject: [PATCH 249/737] [artifactory-release] Release version 3.0.5 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index d12bbe7c8b..0a790028b8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.5-SNAPSHOT +version=3.0.5 org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true From 722fad8ee72a8861a4a2a30c66d9464416c71192 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Sat, 17 Jun 2023 14:14:40 +0000 Subject: [PATCH 250/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 0a790028b8..c4fb1a01b8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.5 +version=3.0.6-SNAPSHOT org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true From 1fdfe650ac1b6b28f11543af026c94b5128f1ba5 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 12 Jul 2023 14:56:48 -0400 Subject: [PATCH 251/737] GH-2482: Option for Containers to Stop Immediately Resolves https://github.com/spring-projects/spring-amqp/issues/2482 `forceStop` means stop after the current record and requeue all prefetched. Just close the channel - canceling the consumer first causes a race condition which could allow another exclusive or single-active consumer to start processing while this container is still running. Also support async stop on DMLC (previously only available on the SMLC). **cherry-pick to 2.4.x** --- .../AbstractMessageListenerContainer.java | 43 ++++++++- .../listener/BlockingQueueConsumer.java | 18 ++-- .../DirectMessageListenerContainer.java | 89 +++++++++++++------ .../SimpleMessageListenerContainer.java | 29 +++--- ...sageListenerContainerIntegrationTests.java | 46 ++++++++++ ...ageListenerContainerIntegration2Tests.java | 44 ++++++++- src/reference/asciidoc/amqp.adoc | 16 ++++ 7 files changed, 229 insertions(+), 56 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index 88ef0ef456..f46dc61505 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -136,10 +136,12 @@ public abstract class AbstractMessageListenerContainer extends ObservableListene private final Map consumerArgs = new HashMap<>(); - private ContainerDelegate proxy = this.delegate; - private final AtomicBoolean logDeclarationException = new AtomicBoolean(true); + protected final AtomicBoolean stopNow = new AtomicBoolean(); // NOSONAR + + private ContainerDelegate proxy = this.delegate; + private long shutdownTimeout = DEFAULT_SHUTDOWN_TIMEOUT; private ApplicationEventPublisher applicationEventPublisher; @@ -245,6 +247,8 @@ public abstract class AbstractMessageListenerContainer extends ObservableListene @Nullable private RabbitListenerObservationConvention observationConvention; + private boolean forceStop; + @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; @@ -1153,6 +1157,25 @@ protected MessageAckListener getMessageAckListener() { return this.messageAckListener; } + /** + * Stop container after current message(s) are processed and requeue any prefetched. + * @return true to stop when current message(s) are processed. + * @since 2.4.14 + */ + protected boolean isForceStop() { + return this.forceStop; + } + + /** + * Set to true to stop the container after the current message(s) are processed and + * requeue any prefetched. Useful when using exclusive or single-active consumers. + * @param forceStop true to stop when current messsage(s) are processed. + * @since 2.4.14 + */ + public void setForceStop(boolean forceStop) { + this.forceStop = forceStop; + } + /** * Delegates to {@link #validateConfiguration()} and {@link #initialize()}. */ @@ -1302,7 +1325,21 @@ protected void setNotRunning() { * A shared Rabbit Connection, if any, will automatically be closed afterwards. * @see #shutdown() */ - protected abstract void doShutdown(); + protected void doShutdown() { + shutdownAndWaitOrCallback(null); + } + + @Override + public void stop(Runnable callback) { + shutdownAndWaitOrCallback(() -> { + setNotRunning(); + callback.run(); + }); + } + + protected void shutdownAndWaitOrCallback(@Nullable Runnable callback) { + } + /** * @return Whether this container is currently active, that is, whether it has been set up but not shut down yet. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java index cf7eacdcf1..757e3b63e4 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -784,11 +784,17 @@ public synchronized void stop() { if (logger.isDebugEnabled()) { logger.debug("Closing Rabbit Channel: " + this.channel); } - RabbitUtils.setPhysicalCloseRequired(this.channel, true); - ConnectionFactoryUtils.releaseResources(this.resourceHolder); - this.deliveryTags.clear(); - this.consumers.clear(); - this.queue.clear(); // in case we still have a client thread blocked + forceCloseAndClearQueue(); + } + + public void forceCloseAndClearQueue() { + if (this.channel != null && this.channel.isOpen()) { + RabbitUtils.setPhysicalCloseRequired(this.channel, true); + ConnectionFactoryUtils.releaseResources(this.resourceHolder); + this.deliveryTags.clear(); + this.consumers.clear(); + this.queue.clear(); // in case we still have a client thread blocked + } } /** diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java index cabbe13dc6..6e9aca9f5e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 the original author or authors. + * Copyright 2016-2023 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. @@ -813,7 +813,7 @@ else if (this.logger.isWarnEnabled()) { } @Override - protected void doShutdown() { + protected void shutdownAndWaitOrCallback(@Nullable Runnable callback) { LinkedList canceledConsumers = null; boolean waitForConsumers = false; synchronized (this.consumersMonitor) { @@ -826,36 +826,53 @@ protected void doShutdown() { } } if (waitForConsumers) { - try { - if (this.cancellationLock.await(getShutdownTimeout(), TimeUnit.MILLISECONDS)) { - this.logger.info("Successfully waited for consumers to finish."); - } - else { - this.logger.info("Consumers not finished."); - if (isForceCloseChannel()) { - canceledConsumers.forEach(consumer -> { - String eventMessage = "Closing channel for unresponsive consumer: " + consumer; - if (logger.isWarnEnabled()) { - logger.warn(eventMessage); - } - consumer.cancelConsumer(eventMessage); - }); + LinkedList consumersToWait = canceledConsumers; + Runnable awaitShutdown = () -> { + try { + if (this.cancellationLock.await(getShutdownTimeout(), TimeUnit.MILLISECONDS)) { + this.logger.info("Successfully waited for consumers to finish."); + } + else { + this.logger.info("Consumers not finished."); + if (isForceCloseChannel() || this.stopNow.get()) { + consumersToWait.forEach(consumer -> { + String eventMessage = "Closing channel for unresponsive consumer: " + consumer; + if (logger.isWarnEnabled()) { + logger.warn(eventMessage); + } + consumer.cancelConsumer(eventMessage); + }); + } } } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + this.logger.warn("Interrupted waiting for consumers. Continuing with shutdown."); + } + finally { + this.startedLatch = new CountDownLatch(1); + this.started = false; + this.aborted = false; + this.hasStopped = true; + } + this.stopNow.set(false); + runCallbackIfNotNull(callback); + }; + if (callback == null) { + awaitShutdown.run(); } - catch (InterruptedException e) { - Thread.currentThread().interrupt(); - this.logger.warn("Interrupted waiting for consumers. Continuing with shutdown."); - } - finally { - this.startedLatch = new CountDownLatch(1); - this.started = false; - this.aborted = false; - this.hasStopped = true; + else { + getTaskExecutor().execute(awaitShutdown); } } } + private void runCallbackIfNotNull(@Nullable Runnable callback) { + if (callback != null) { + callback.run(); + } + } + /** * Must hold this.consumersMonitor. * @param consumers a copy of this.consumers. @@ -863,7 +880,12 @@ protected void doShutdown() { private void actualShutDown(List consumers) { Assert.state(getTaskExecutor() != null, "Cannot shut down if not initialized"); this.logger.debug("Shutting down"); - consumers.forEach(this::cancelConsumer); + if (isForceStop()) { + this.stopNow.set(true); + } + else { + consumers.forEach(this::cancelConsumer); + } this.consumers.clear(); this.consumersByQueue.clear(); this.logger.debug("All consumers canceled"); @@ -1031,6 +1053,10 @@ int incrementAndGetEpoch() { public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body) { + if (!getChannel().isOpen()) { + this.logger.debug("Discarding prefetch, channel closed"); + return; + } MessageProperties messageProperties = getMessagePropertiesConverter().toMessageProperties(properties, envelope, "UTF-8"); messageProperties.setConsumerTag(consumerTag); @@ -1072,6 +1098,9 @@ public void handleDelivery(String consumerTag, Envelope envelope, // NOSONAR } } + if (DirectMessageListenerContainer.this.stopNow.get()) { + closeChannel(); + } } private void executeListenerInTransaction(Object data, long deliveryTag) { @@ -1308,11 +1337,15 @@ void cancelConsumer(final String eventMessage) { } private void finalizeConsumer() { + closeChannel(); + DirectMessageListenerContainer.this.cancellationLock.release(this); + consumerRemoved(this); + } + + private void closeChannel() { RabbitUtils.setPhysicalCloseRequired(getChannel(), true); RabbitUtils.closeChannel(getChannel()); RabbitUtils.closeConnection(this.connection); - DirectMessageListenerContainer.this.cancellationLock.release(this); - consumerRemoved(this); } @Override diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java index 9f93320242..1eeec52810 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java @@ -607,19 +607,7 @@ private void waitForConsumersToStart(Set process } @Override - protected void doShutdown() { - shutdownAndWaitOrCallback(null); - } - - @Override - public void stop(Runnable callback) { - shutdownAndWaitOrCallback(() -> { - setNotRunning(); - callback.run(); - }); - } - - private void shutdownAndWaitOrCallback(@Nullable Runnable callback) { + protected void shutdownAndWaitOrCallback(@Nullable Runnable callback) { Thread thread = this.containerStoppingForAbort.get(); if (thread != null && !thread.equals(Thread.currentThread())) { logger.info("Shutdown ignored - container is stopping due to an aborted consumer"); @@ -631,9 +619,14 @@ private void shutdownAndWaitOrCallback(@Nullable Runnable callback) { synchronized (this.consumersMonitor) { if (this.consumers != null) { Iterator consumerIterator = this.consumers.iterator(); + if (isForceStop()) { + this.stopNow.set(true); + } while (consumerIterator.hasNext()) { BlockingQueueConsumer consumer = consumerIterator.next(); - consumer.basicCancel(true); + if (!isForceStop()) { + consumer.basicCancel(true); + } canceledConsumers.add(consumer); consumerIterator.remove(); if (consumer.declaring) { @@ -657,7 +650,7 @@ private void shutdownAndWaitOrCallback(@Nullable Runnable callback) { } else { logger.info("Workers not finished."); - if (isForceCloseChannel()) { + if (isForceCloseChannel() || this.stopNow.get()) { canceledConsumers.forEach(consumer -> { if (logger.isWarnEnabled()) { logger.warn("Closing channel for unresponsive consumer: " + consumer); @@ -676,7 +669,7 @@ private void shutdownAndWaitOrCallback(@Nullable Runnable callback) { this.consumers = null; this.cancellationLock.deactivate(); } - + this.stopNow.set(false); runCallbackIfNotNull(callback); }; if (callback == null) { @@ -1323,6 +1316,10 @@ public void run() { // NOSONAR - line count private void mainLoop() throws Exception { // NOSONAR Exception try { + if (SimpleMessageListenerContainer.this.stopNow.get()) { + this.consumer.forceCloseAndClearQueue(); + return; + } boolean receivedOk = receiveAndExecute(this.consumer); // At least one message received if (SimpleMessageListenerContainer.this.maxConcurrentConsumers != null) { checkAdjust(receivedOk); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java index 71e1403ddf..62f111cc45 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java @@ -51,6 +51,7 @@ import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.QueueInformation; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.Connection; import org.springframework.amqp.rabbit.connection.ConnectionFactory; @@ -90,6 +91,7 @@ */ @RabbitAvailable(queues = { DirectMessageListenerContainerIntegrationTests.Q1, DirectMessageListenerContainerIntegrationTests.Q2, + DirectMessageListenerContainerIntegrationTests.Q3, DirectMessageListenerContainerIntegrationTests.EQ1, DirectMessageListenerContainerIntegrationTests.EQ2, DirectMessageListenerContainerIntegrationTests.DLQ1 }) @@ -102,6 +104,8 @@ public class DirectMessageListenerContainerIntegrationTests { public static final String Q2 = "testQ2.DirectMessageListenerContainerIntegrationTests"; + public static final String Q3 = "testQ3.DirectMessageListenerContainerIntegrationTests"; + public static final String EQ1 = "eventTestQ1.DirectMessageListenerContainerIntegrationTests"; public static final String EQ2 = "eventTestQ2.DirectMessageListenerContainerIntegrationTests"; @@ -792,6 +796,48 @@ public void onMessage(Message message) { assertThat(ackDeliveryTag.get()).isEqualTo(1); } + @Test + void forceStop() { + CountDownLatch latch1 = new CountDownLatch(1); + CachingConnectionFactory cf = new CachingConnectionFactory("localhost"); + DirectMessageListenerContainer container = new DirectMessageListenerContainer(cf); + container.setMessageListener((ChannelAwareMessageListener) (msg, chan) -> { + latch1.await(10, TimeUnit.SECONDS); + }); + RabbitTemplate template = new RabbitTemplate(cf); + try { + container.setQueueNames(Q3); + container.setForceStop(true); + template.convertAndSend(Q3, "one"); + template.convertAndSend(Q3, "two"); + template.convertAndSend(Q3, "three"); + template.convertAndSend(Q3, "four"); + template.convertAndSend(Q3, "five"); + await().untilAsserted(() -> { + QueueInformation queueInfo = admin.getQueueInfo(Q3); + assertThat(queueInfo).isNotNull(); + assertThat(queueInfo.getMessageCount()).isEqualTo(5); + }); + container.start(); + await().untilAsserted(() -> { + QueueInformation queueInfo = admin.getQueueInfo(Q3); + assertThat(queueInfo).isNotNull(); + assertThat(queueInfo.getMessageCount()).isEqualTo(0); + }); + container.stop(() -> { + }); + latch1.countDown(); + await().untilAsserted(() -> { + QueueInformation queueInfo = admin.getQueueInfo(Q3); + assertThat(queueInfo).isNotNull(); + assertThat(queueInfo.getMessageCount()).isEqualTo(4); + }); + } + finally { + container.stop(); + } + } + @Test public void testMessageAckListenerWithBatchAck() throws Exception { final AtomicInteger calledTimes = new AtomicInteger(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java index d80eb17b8e..5599b99a9c 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -57,6 +57,7 @@ import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.QueueInformation; import org.springframework.amqp.event.AmqpEvent; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.Connection; @@ -66,7 +67,6 @@ import org.springframework.amqp.rabbit.core.RabbitAdmin; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.junit.BrokerTestUtils; -import org.springframework.amqp.rabbit.junit.LongRunning; import org.springframework.amqp.rabbit.junit.RabbitAvailable; import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter; import org.springframework.amqp.rabbit.listener.adapter.ReplyingMessageListener; @@ -95,7 +95,7 @@ */ @RabbitAvailable(queues = { SimpleMessageListenerContainerIntegration2Tests.TEST_QUEUE, SimpleMessageListenerContainerIntegration2Tests.TEST_QUEUE_1 }) -@LongRunning +//@LongRunning public class SimpleMessageListenerContainerIntegration2Tests { public static final String TEST_QUEUE = "test.queue.SimpleMessageListenerContainerIntegration2Tests"; @@ -747,6 +747,44 @@ public void testMessageAckListenerWithBatchAck() throws Exception { assertThat(ackDeliveryTag.get()).isEqualTo(messageCount); } + @Test + void forceStop() { + CountDownLatch latch1 = new CountDownLatch(1); + this.container = createContainer((ChannelAwareMessageListener) (msg, chan) -> { + latch1.await(10, TimeUnit.SECONDS); + }, false, TEST_QUEUE); + try { + this.container.setForceStop(true); + this.template.convertAndSend(TEST_QUEUE, "one"); + this.template.convertAndSend(TEST_QUEUE, "two"); + this.template.convertAndSend(TEST_QUEUE, "three"); + this.template.convertAndSend(TEST_QUEUE, "four"); + this.template.convertAndSend(TEST_QUEUE, "five"); + await().untilAsserted(() -> { + QueueInformation queueInfo = admin.getQueueInfo(TEST_QUEUE); + assertThat(queueInfo).isNotNull(); + assertThat(queueInfo.getMessageCount()).isEqualTo(5); + }); + this.container.start(); + await().untilAsserted(() -> { + QueueInformation queueInfo = admin.getQueueInfo(TEST_QUEUE); + assertThat(queueInfo).isNotNull(); + assertThat(queueInfo.getMessageCount()).isEqualTo(0); + }); + this.container.stop(() -> { + }); + latch1.countDown(); + await().untilAsserted(() -> { + QueueInformation queueInfo = admin.getQueueInfo(TEST_QUEUE); + assertThat(queueInfo).isNotNull(); + assertThat(queueInfo.getMessageCount()).isEqualTo(4); + }); + } + finally { + this.container.stop(); + } + } + private boolean containerStoppedForAbortWithBadListener() throws InterruptedException { Log logger = spy(TestUtils.getPropertyValue(container, "logger", Log.class)); new DirectFieldAccessor(container).setPropertyValue("logger", logger); diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index 404c70232e..21c0adc2fd 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -3527,6 +3527,10 @@ Starting with version 1.5, you can now assign a `group` to the container on the This provides a mechanism to get a reference to a subset of containers. Adding a `group` attribute causes a bean of type `Collection` to be registered with the context with the group name. +By default, stopping a container will cancel the consumer and process all prefetched messages before stopping. +Starting with versions 2.4.14, 3.0.6, you can set the <> 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. + [[receiving-batch]] ===== @RabbitListener with Batching @@ -6209,6 +6213,18 @@ a|image::images/tickmark.png[] a|image::images/tickmark.png[] a| +|[[forceStop]]<> + +(N/A) + +|Set to true to stop (when the container is stopped) after the current record is processed; causing all prefetched messages to be requeued. +By default, the container will cancel the consumer and process all prefetched messages before stopping. +Since versions 2.4.14, 3.0.6 +Defaults to `false`. + +a|image::images/tickmark.png[] +a|image::images/tickmark.png[] +a| + |[[globalQos]]<> + (global-qos) From a0af1af6edd479ae1d39b7b221107cc5cb37d908 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 12 Jul 2023 15:47:36 -0400 Subject: [PATCH 252/737] Fix DMLC Unknown Host Test Use a custom address resolver instead of using DNS. --- .../DirectMessageListenerContainerIntegrationTests.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java index 62f111cc45..6108a804b8 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import java.net.UnknownHostException; import java.time.Duration; import java.util.ArrayList; import java.util.List; @@ -184,6 +185,9 @@ public void testSimple() throws Exception { @Test public void testBadHost() throws InterruptedException { CachingConnectionFactory cf = new CachingConnectionFactory("this.host.does.not.exist"); + cf.setAddressResolver(() -> { + throw new UnknownHostException("Test Unknown Host"); + }); ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setThreadNamePrefix("client-"); executor.afterPropertiesSet(); From 6539f9c2bd0f9d39fb78c363f9c969979275c9d6 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 12 Jul 2023 16:49:34 -0400 Subject: [PATCH 253/737] Upgrade amqp-client Version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index eec11ee2fa..d965d913bb 100644 --- a/build.gradle +++ b/build.gradle @@ -65,7 +65,7 @@ ext { micrometerTracingVersion = '1.0.7' mockitoVersion = '4.8.1' rabbitmqStreamVersion = '0.8.0' - rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.16.0' + rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.16.1' reactorVersion = '2022.0.8' snappyVersion = '1.1.8.4' springDataVersion = '2022.0.7' From d44a8d0074d849309a0229d3c11340975986036b Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 13 Jul 2023 15:00:23 -0400 Subject: [PATCH 254/737] GH-2482: Fix Async Container Stop - Stop with callback did not reset `active` flag (both containers) - DMLC did not release the `cancellationLock` - Also restore `@LongRunning` --- .../AbstractMessageListenerContainer.java | 19 +++++++++++++------ .../DirectMessageListenerContainer.java | 2 +- ...sageListenerContainerIntegrationTests.java | 10 +++++++++- ...ageListenerContainerIntegration2Tests.java | 3 ++- .../SimpleMessageListenerContainerTests.java | 2 ++ 5 files changed, 27 insertions(+), 9 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index f46dc61505..efb9b98d9e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -1276,9 +1276,19 @@ public void initialize() { } /** - * Stop the shared Connection, call {@link #doShutdown()}, and close this container. + * Stop the shared Connection, call {@link #shutdown(Runnable)}, and close this + * container. */ public void shutdown() { + shutdown(null); + } + + /** + * Stop the shared Connection, call {@link #shutdownAndWaitOrCallback(Runnable)}, and + * close this container. + * @param callback an optional {@link Runnable} to call when the stop is complete. + */ + public void shutdown(@Nullable Runnable callback) { synchronized (this.lifecycleMonitor) { if (!isActive()) { logger.debug("Shutdown ignored - container is not active already"); @@ -1293,7 +1303,7 @@ public void shutdown() { // Shut down the invokers. try { - doShutdown(); + shutdownAndWaitOrCallback(callback); } catch (Exception ex) { throw convertRabbitAccessException(ex); @@ -1331,10 +1341,7 @@ protected void doShutdown() { @Override public void stop(Runnable callback) { - shutdownAndWaitOrCallback(() -> { - setNotRunning(); - callback.run(); - }); + shutdown(callback); } protected void shutdownAndWaitOrCallback(@Nullable Runnable callback) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java index 6e9aca9f5e..5eb2785318 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java @@ -1338,7 +1338,6 @@ void cancelConsumer(final String eventMessage) { private void finalizeConsumer() { closeChannel(); - DirectMessageListenerContainer.this.cancellationLock.release(this); consumerRemoved(this); } @@ -1346,6 +1345,7 @@ private void closeChannel() { RabbitUtils.setPhysicalCloseRequired(getChannel(), true); RabbitUtils.closeChannel(getChannel()); RabbitUtils.closeConnection(this.connection); + DirectMessageListenerContainer.this.cancellationLock.release(this); } @Override diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java index 6108a804b8..8fe662f17c 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java @@ -801,7 +801,7 @@ public void onMessage(Message message) { } @Test - void forceStop() { + void forceStop() throws InterruptedException { CountDownLatch latch1 = new CountDownLatch(1); CachingConnectionFactory cf = new CachingConnectionFactory("localhost"); DirectMessageListenerContainer container = new DirectMessageListenerContainer(cf); @@ -812,6 +812,7 @@ void forceStop() { try { container.setQueueNames(Q3); container.setForceStop(true); + container.setShutdownTimeout(20_000L); template.convertAndSend(Q3, "one"); template.convertAndSend(Q3, "two"); template.convertAndSend(Q3, "three"); @@ -828,14 +829,21 @@ void forceStop() { assertThat(queueInfo).isNotNull(); assertThat(queueInfo.getMessageCount()).isEqualTo(0); }); + CountDownLatch latch2 = new CountDownLatch(1); + long t1 = System.currentTimeMillis(); container.stop(() -> { + latch2.countDown(); }); latch1.countDown(); + assertThat(System.currentTimeMillis() - t1).isLessThan(5_000L); await().untilAsserted(() -> { QueueInformation queueInfo = admin.getQueueInfo(Q3); assertThat(queueInfo).isNotNull(); assertThat(queueInfo.getMessageCount()).isEqualTo(4); }); + assertThat(latch2.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(container.isActive()).isFalse(); + assertThat(container.isRunning()).isFalse(); } finally { container.stop(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java index 5599b99a9c..0c247fc972 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java @@ -67,6 +67,7 @@ import org.springframework.amqp.rabbit.core.RabbitAdmin; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.junit.BrokerTestUtils; +import org.springframework.amqp.rabbit.junit.LongRunning; import org.springframework.amqp.rabbit.junit.RabbitAvailable; import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter; import org.springframework.amqp.rabbit.listener.adapter.ReplyingMessageListener; @@ -95,7 +96,7 @@ */ @RabbitAvailable(queues = { SimpleMessageListenerContainerIntegration2Tests.TEST_QUEUE, SimpleMessageListenerContainerIntegration2Tests.TEST_QUEUE_1 }) -//@LongRunning +@LongRunning public class SimpleMessageListenerContainerIntegration2Tests { public static final String TEST_QUEUE = "test.queue.SimpleMessageListenerContainerIntegration2Tests"; diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java index 3cabcfe319..204df12abb 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java @@ -437,6 +437,7 @@ public void testCallbackIsRunOnStopAlsoWhenNoConsumerIsActive() throws Interrupt ConnectionFactory connectionFactory = mock(ConnectionFactory.class); SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); + ReflectionTestUtils.setField(container, "active", Boolean.TRUE); final CountDownLatch countDownLatch = new CountDownLatch(1); container.stop(countDownLatch::countDown); @@ -449,6 +450,7 @@ public void testCallbackIsRunOnStopAlsoWhenContainerIsStoppingForAbort() throws SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); ReflectionTestUtils.setField(container, "containerStoppingForAbort", new AtomicReference<>(new Thread())); + ReflectionTestUtils.setField(container, "active", Boolean.TRUE); final CountDownLatch countDownLatch = new CountDownLatch(1); container.stop(countDownLatch::countDown); From dd1aca20c0a94556529e6c8d123fba45f5fe620e Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 17 Jul 2023 09:21:46 -0400 Subject: [PATCH 255/737] Upgrade Spr. Framework, Data, Micrometer, Reactor (#2489) --- build.gradle | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index d965d913bb..aa40dd44ec 100644 --- a/build.gradle +++ b/build.gradle @@ -61,16 +61,16 @@ ext { logbackVersion = '1.4.4' lz4Version = '1.8.0' micrometerDocsVersion = '1.0.2' - micrometerVersion = '1.10.8' - micrometerTracingVersion = '1.0.7' + micrometerVersion = '1.10.9' + micrometerTracingVersion = '1.0.8' mockitoVersion = '4.8.1' rabbitmqStreamVersion = '0.8.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.16.1' - reactorVersion = '2022.0.8' + reactorVersion = '2022.0.9' snappyVersion = '1.1.8.4' - springDataVersion = '2022.0.7' + springDataVersion = '2022.0.8' springRetryVersion = '2.0.2' - springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.10' + springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.11' testcontainersVersion = '1.17.6' zstdJniVersion = '1.5.0-2' From 52b2c27dc46e4c2437337835f6ea4f7e30f02dfe Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 17 Jul 2023 15:30:28 +0000 Subject: [PATCH 256/737] [artifactory-release] Release version 3.0.6 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index c4fb1a01b8..05c7b335e1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.6-SNAPSHOT +version=3.0.6 org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true From 58448ad5d3624bc57cbb2c261f83719aed7de81d Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 17 Jul 2023 15:30:30 +0000 Subject: [PATCH 257/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 05c7b335e1..267ad2f3c1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.6 +version=3.0.7-SNAPSHOT org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true From fa944397e702453156f057f3ccd72bbbc9a58778 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 19 Jul 2023 13:38:43 -0400 Subject: [PATCH 258/737] GH-2490: Add forceStop to Container Factories Resolves https://github.com/spring-projects/spring-amqp/issues/2490 **cherry-pick to 2.4.x** --- .../AbstractRabbitListenerContainerFactory.java | 15 ++++++++++++++- .../RabbitListenerContainerFactoryTests.java | 6 +++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java index fbeff0bb1b..f1ec63668a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java @@ -117,6 +117,8 @@ public abstract class AbstractRabbitListenerContainerFactory c.setShutdownTimeout(10_000)); + this.factory.setForceStop(true); assertThat(this.factory.getAdviceChain()).isEqualTo(new Advice[]{advice}); @@ -150,6 +151,7 @@ public void createContainerFullConfig() { assertThat(actualAfterReceivePostProcessors.size()).as("Wrong number of afterReceivePostProcessors").isEqualTo(1); assertThat(actualAfterReceivePostProcessors.get(0)).as("Wrong advice").isSameAs(afterReceivePostProcessor); assertThat(fieldAccessor.getPropertyValue("globalQos")).isEqualTo(true); + assertThat(TestUtils.getPropertyValue(container, "forceStop", Boolean.class)).isTrue(); } @Test @@ -176,6 +178,7 @@ public void createDirectContainerFullConfig() { this.direct.setMessagesPerAck(5); this.direct.setAckTimeout(3L); this.direct.setAfterReceivePostProcessors(afterReceivePostProcessor); + this.direct.setForceStop(true); assertThat(this.direct.getAdviceChain()).isEqualTo(new Advice[]{advice}); @@ -207,6 +210,7 @@ public void createDirectContainerFullConfig() { List actualAfterReceivePostProcessors = (List) fieldAccessor.getPropertyValue("afterReceivePostProcessors"); assertThat(actualAfterReceivePostProcessors.size()).as("Wrong number of afterReceivePostProcessors").isEqualTo(1); assertThat(actualAfterReceivePostProcessors.get(0)).as("Wrong afterReceivePostProcessor").isSameAs(afterReceivePostProcessor); + assertThat(TestUtils.getPropertyValue(container, "forceStop", Boolean.class)).isTrue(); } private void setBasicConfig(AbstractRabbitListenerContainerFactory factory) { From 9fb108342b3bc295cff4b84c753e1f618e16d683 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 24 Jul 2023 13:56:44 -0400 Subject: [PATCH 259/737] GH-2493: Sync Gradle .module File to Maven (#2494) Resolves https://github.com/spring-projects/spring-amqp/issues/2493 --- .github/release-files-spec.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/release-files-spec.json b/.github/release-files-spec.json index 4ceba9f6d9..ee43685f90 100644 --- a/.github/release-files-spec.json +++ b/.github/release-files-spec.json @@ -16,6 +16,9 @@ }, { "name": {"$match": "*.jar"} + }, + { + "name": {"$match": "*.module"} } ] } From 20c39f48d47d7a890a915ca58ba6036c3a7f6812 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 24 Jul 2023 18:35:14 +0000 Subject: [PATCH 260/737] [artifactory-release] Release version 3.0.7 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 267ad2f3c1..037671109b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.7-SNAPSHOT +version=3.0.7 org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true From b64f82c48e003df02daa64a8da808e11c493d4a9 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 24 Jul 2023 18:35:16 +0000 Subject: [PATCH 261/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 037671109b..60fcf260b1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.7 +version=3.0.8-SNAPSHOT org.gradlee.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true From 3675a6c8d75d18d7805c77e4db6574ac3e09e656 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 24 Jul 2023 16:34:21 -0400 Subject: [PATCH 262/737] Update Manifest Vendor --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index aa40dd44ec..6ed218813f 100644 --- a/build.gradle +++ b/build.gradle @@ -329,7 +329,7 @@ configure(javaProjects) { subproject -> 'Created-By': "JDK ${System.properties['java.version']} (${System.properties['java.specification.vendor']})", 'Implementation-Title': subproject.name, 'Implementation-Vendor-Id': subproject.group, - 'Implementation-Vendor': 'Pivotal Software, Inc.', + 'Implementation-Vendor': 'VMware Inc.', 'Implementation-URL': linkHomepage, 'Automatic-Module-Name': subproject.name.replace('-', '.') // for Jigsaw ) From a18470d9f85ff0513b7f7c0482e04bb92b048623 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 26 Jul 2023 13:01:30 -0400 Subject: [PATCH 263/737] GH-2495: Add maxInboundMessageBodySize to RCFB Resolves https://github.com/spring-projects/spring-amqp/issues/2495 Although the factory bean has... ```java /** * Access the connection factory to set any other properties not supported by * this factory bean. * @return the connection factory. * @since 1.7.14 */ public ConnectionFactory getRabbitConnectionFactory() { ``` ...this new CF property could be a breaking change for users that configure the factory bean using XML. **cherry-pick to 2.4.x** --- .../connection/RabbitConnectionFactoryBean.java | 14 ++++++++++++-- .../amqp/rabbit/connection/SSLConnectionTests.java | 4 +++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitConnectionFactoryBean.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitConnectionFactoryBean.java index 08b1dfd470..d12361ee1d 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitConnectionFactoryBean.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitConnectionFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -66,7 +66,7 @@ * using the supplied properties and intializes key and trust manager factories, using * algorithm {@code SunX509} by default. These are then used to initialize an * {@link SSLContext} using the {@link #setSslAlgorithm(String) sslAlgorithm} (default - * TLSv1.1). + * TLSv1.2, falling back to TLSv1.1, if 1.2 is not available). *

* Override {@link #createSSLContext()} to create and/or perform further modification of * the context. @@ -672,6 +672,16 @@ public void setEnableHostnameVerification(boolean enable) { this.enableHostnameVerification = enable; } + /** + * Set the maximum body size of inbound (received) messages in bytes. + * @param maxInboundMessageBodySize the maximum size. + * @since 2.4.15 + * @see com.rabbitmq.client.ConnectionFactory#setMaxInboundMessageBodySize(int) + */ + public void setMaxInboundMessageBodySize(int maxInboundMessageBodySize) { + this.connectionFactory.setMaxInboundMessageBodySize(maxInboundMessageBodySize); + } + protected String getKeyStoreAlgorithm() { return this.keyStoreAlgorithm; } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SSLConnectionTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SSLConnectionTests.java index d3c50a58de..e4e4f926be 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SSLConnectionTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SSLConnectionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2023 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. @@ -76,6 +76,7 @@ public void test() throws Exception { @Test public void testAlgNoProps() throws Exception { RabbitConnectionFactoryBean fb = new RabbitConnectionFactoryBean(); + fb.setMaxInboundMessageBodySize(1000); ConnectionFactory rabbitCf = spy(TestUtils.getPropertyValue(fb, "connectionFactory", ConnectionFactory.class)); new DirectFieldAccessor(fb).setPropertyValue("connectionFactory", rabbitCf); fb.setUseSSL(true); @@ -83,6 +84,7 @@ public void testAlgNoProps() throws Exception { fb.afterPropertiesSet(); fb.getObject(); verify(rabbitCf).useSslProtocol(Mockito.any(SSLContext.class)); + assertThat(rabbitCf).hasFieldOrPropertyWithValue("maxInboundMessageBodySize", 1000); } @Test From bcfbce68f018119324afc8c801f2615e8297daac Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 27 Jul 2023 09:28:20 -0400 Subject: [PATCH 264/737] Remove Gitter badge from README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8dc67cf47d..63d1254fbb 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Spring AMQP [](https://build.spring.io/browse/AAMQP-MAIN) [![Join the chat at https://gitter.im/spring-projects/spring-amqp](https://badges.gitter.im/spring-projects/spring-amqp.svg)](https://gitter.im/spring-projects/spring-amqp?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +Spring AMQP [](https://build.spring.io/browse/AAMQP-MAIN) =========== This project provides support for using Spring and Java with [AMQP 0.9.1](https://www.rabbitmq.com/amqp-0-9-1-reference.html), and in particular [RabbitMQ](https://www.rabbitmq.com/). From d1dbf103587e3cb6d844e1f028e0ec7e09177b94 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 27 Jul 2023 11:39:35 -0400 Subject: [PATCH 265/737] Add Gradle Enterprise Build Scanning * README Polishing. * More README polishing. --- README.md | 49 +++++++++++++++++-------------------------------- settings.gradle | 5 +++++ 2 files changed, 22 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 63d1254fbb..8da434b771 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -Spring AMQP [](https://build.spring.io/browse/AAMQP-MAIN) +Spring AMQP [](https://build.spring.io/browse/AAMQP-MAIN) +[![Revved up by Gradle Enterprise](https://img.shields.io/badge/Revved%20up%20by-Gradle%20Enterprise-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.spring.io/scans?search.rootProjectNames=spring-amqp) =========== This project provides support for using Spring and Java with [AMQP 0.9.1](https://www.rabbitmq.com/amqp-0-9-1-reference.html), and in particular [RabbitMQ](https://www.rabbitmq.com/). @@ -58,15 +59,14 @@ Once complete, you may then import the projects into Eclipse as usual: Browse to the *'spring-amqp'* root directory. All projects should import free of errors. -# Using SpringSource Tool Suite™ (STS) +# Using Spring Tools -Using the STS Gradle Support, you can directly import Gradle projects, without having to generate Eclipse metadata first (since STS 2.7.M1). Please make sure you have the Gradle STS Extension installed - Please see the [installation instructions](https://docs.spring.io/sts/docs/latest/reference/html/gradle/installation.html) for details. +Using the STS Gradle Support, you can directly import Gradle projects, without having to generate Eclipse metadata first. +Please see the [Spring Tools Home Page](https://spring.io/tools). -1. Select *File -> Import -> Gradle Project* +1. Select *File -> Import -> Existing Gradle Project* 2. Browse to the Spring AMQP Root Folder -3. Click on **Build Model** -4. Select the projects you want to import -5. Press **Finish** +3. Click on **Finish** # Using IntelliJ IDEA @@ -74,45 +74,30 @@ To generate IDEA metadata (.iml and .ipr files), do the following: ./gradlew idea -## Distribution Contents - -If you downloaded the full Spring AMQP distribution or if you created the distribution using `./gradlew dist`, you will see the following directory structure: - - ├── README.md - ├── apache-license.txt - ├── docs - │ ├── api - │ └── reference - ├── epl-license.txt - ├── libs - ├── notice.txt - └── schema - └── rabbit - -The binary JARs and the source code are available in the **libs**. The reference manual and javadocs are located in the **docs** directory. - ## Changelog -Lists of issues addressed per release can be found in [JIRA](https://jira.spring.io/browse/AMQP#selectedTab=com.atlassian.jira.plugin.system.project%3Aversions-panel). +Lists of issues addressed per release can be found in [Github](https://github.com/spring-projects/spring-amqp/releases). ## Additional Resources -* [Spring AMQP Homepage](https://www.springsource.org/spring-amqp) +* [Spring AMQP Homepage](https://spring.io/projects/spring-amqp) * [Spring AMQP Source](https://github.com/SpringSource/spring-amqp) * [Spring AMQP Samples](https://github.com/SpringSource/spring-amqp-samples) -* [Spring AMQP Forum](https://forum.spring.io/) * [StackOverflow](https://stackoverflow.com/questions/tagged/spring-amqp) # Contributing to Spring AMQP Here are some ways for you to get involved in the community: -* Get involved with the Spring community on the Spring Community Forums. Please help out on the [forum](https://forum.spring.io/) by responding to questions and joining the debate. -* Create [JIRA](https://jira.spring.io/browse/AMQP) tickets for bugs and new features and comment and vote on the ones that you are interested in. -* Github is for social coding: if you want to write code, we encourage contributions through pull requests from [forks of this repository](https://help.github.com/forking/). If you want to contribute code this way, please reference a JIRA ticket as well covering the specific issue you are addressing. -* Watch for upcoming articles on Spring by [subscribing](https://www.springsource.org/node/feed) to springframework.org +* Get involved with the Spring community on Stack Overflow by responding to questions and joining the debate. + +* Create Github issues for bugs and new features and comment and vote on the ones that you are interested in. +* Github is for social coding: if you want to write code, we encourage contributions through pull requests from [forks of this repository](https://help.github.com/forking/). +If you want to contribute code this way, please reference the specific Github issue you are addressing. -Before we accept a non-trivial patch or pull request we will need you to sign the [contributor's agreement](https://support.springsource.com/spring_committer_signup). Signing the contributor's agreement does not grant anyone commit rights to the main repository, but it does mean that we can accept your contributions, and you will get an author credit if we do. Active contributors might be asked to join the core team, and given the ability to merge pull requests. +Before we accept a non-trivial patch or pull request we will need you to sign the [contributor's agreement](https://cla.pivotal.io/sign/spring). +Signing the contributor's agreement does not grant anyone commit rights to the main repository, but it does mean that we can accept your contributions, and you will get an author credit if we do. +Active contributors might be asked to join the core team, and given the ability to merge pull requests. ## Code Conventions and Housekeeping None of these is essential for a pull request, but they will all help. They can also be added after the original pull request but before a merge. diff --git a/settings.gradle b/settings.gradle index 6be268ea89..ccd2abcf6d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,8 @@ +plugins { + id 'com.gradle.enterprise' version '3.13.4' + id 'io.spring.ge.conventions' version '0.0.13' +} + rootProject.name = 'spring-amqp-dist' include 'spring-amqp' From 12018d51ff10d57880e07d682bb567b56ceb97cf Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 27 Jul 2023 11:41:57 -0400 Subject: [PATCH 266/737] GH-2498: Fix Manual Redeclaration With Dup. Names Resolves https://github.com/spring-projects/spring-amqp/issues/2498 Manual redeclaration logic did not account for an exchange having the same name as a queue. **back port to 2.4.x will have conflicts and requires instanceof polishing** (I will do it after merge). --- .../springframework/amqp/core/AmqpAdmin.java | 12 ++++ .../amqp/rabbit/core/RabbitAdmin.java | 61 ++++++++++++------- .../AbstractMessageListenerContainer.java | 3 +- .../amqp/rabbit/core/RabbitAdminTests.java | 19 +++--- 4 files changed, 65 insertions(+), 30 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpAdmin.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpAdmin.java index 91156cbd4c..76f7e8b6f4 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpAdmin.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpAdmin.java @@ -19,6 +19,7 @@ import java.util.Collections; import java.util.Map; import java.util.Properties; +import java.util.Set; import org.springframework.lang.Nullable; @@ -133,11 +134,22 @@ public interface AmqpAdmin { * Return the manually declared AMQP objects. * @return the manually declared AMQP objects. * @since 2.4.13 + * @deprecated in favor of {@link #getManualDeclarableSet()}. */ + @Deprecated default Map getManualDeclarables() { return Collections.emptyMap(); } + /** + * Return the manually declared AMQP objects. + * @return the manually declared AMQP objects. + * @since 2.4.15 + */ + default Set getManualDeclarableSet() { + return Collections.emptySet(); + } + /** * Initialize the admin. * @since 2.1 diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java index ed1b3bf846..5b957a426b 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java @@ -22,12 +22,12 @@ import java.util.Collections; import java.util.HashMap; import java.util.Iterator; -import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Properties; +import java.util.Set; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -128,7 +128,7 @@ public class RabbitAdmin implements AmqpAdmin, ApplicationContextAware, Applicat private final ConnectionFactory connectionFactory; - private final Map manualDeclarables = Collections.synchronizedMap(new LinkedHashMap<>()); + private final Set manualDeclarables = Collections.synchronizedSet(new LinkedHashSet<>()); private String beanName; @@ -229,7 +229,7 @@ public void declareExchange(final Exchange exchange) { this.rabbitTemplate.execute(channel -> { declareExchanges(channel, exchange); if (this.redeclareManualDeclarations) { - this.manualDeclarables.put(exchange.getName(), exchange); + this.manualDeclarables.add(exchange); } return null; }); @@ -259,12 +259,15 @@ public boolean deleteExchange(final String exchangeName) { } private void removeExchangeBindings(final String exchangeName) { - this.manualDeclarables.remove(exchangeName); synchronized (this.manualDeclarables) { - Iterator> iterator = this.manualDeclarables.entrySet().iterator(); + this.manualDeclarables.stream() + .filter(dec -> dec instanceof Exchange ex && ex.getName().equals(exchangeName)) + .collect(Collectors.toSet()) + .forEach(ex -> this.manualDeclarables.remove(ex)); + Iterator iterator = this.manualDeclarables.iterator(); while (iterator.hasNext()) { - Entry next = iterator.next(); - if (next.getValue() instanceof Binding binding && + Declarable next = iterator.next(); + if (next instanceof Binding binding && ((!binding.isDestinationQueue() && binding.getDestination().equals(exchangeName)) || binding.getExchange().equals(exchangeName))) { iterator.remove(); @@ -296,7 +299,7 @@ public String declareQueue(final Queue queue) { DeclareOk[] declared = declareQueues(channel, queue); String result = declared.length > 0 ? declared[0].getQueue() : null; if (this.redeclareManualDeclarations) { - this.manualDeclarables.put(result, queue); + this.manualDeclarables.add(queue); } return result; }); @@ -356,12 +359,15 @@ public void deleteQueue(final String queueName, final boolean unused, final bool } private void removeQueueBindings(final String queueName) { - this.manualDeclarables.remove(queueName); synchronized (this.manualDeclarables) { - Iterator> iterator = this.manualDeclarables.entrySet().iterator(); + this.manualDeclarables.stream() + .filter(dec -> dec instanceof Queue queue && queue.getName().equals(queueName)) + .collect(Collectors.toSet()) + .forEach(q -> this.manualDeclarables.remove(q)); + Iterator iterator = this.manualDeclarables.iterator(); while (iterator.hasNext()) { - Entry next = iterator.next(); - if (next.getValue() instanceof Binding binding && + Declarable next = iterator.next(); + if (next instanceof Binding binding && (binding.isDestinationQueue() && binding.getDestination().equals(queueName))) { iterator.remove(); } @@ -401,7 +407,7 @@ public void declareBinding(final Binding binding) { this.rabbitTemplate.execute(channel -> { declareBindings(channel, binding); if (this.redeclareManualDeclarations) { - this.manualDeclarables.put(binding.toString(), binding); + this.manualDeclarables.add(binding); } return null; }); @@ -703,7 +709,7 @@ private void redeclareManualDeclarables() { if (this.manualDeclarables.size() > 0) { synchronized (this.manualDeclarables) { this.logger.debug("Redeclaring manually declared Declarables"); - for (Declarable dec : this.manualDeclarables.values()) { + for (Declarable dec : this.manualDeclarables) { if (dec instanceof Queue queue) { declareQueue(queue); } @@ -729,14 +735,27 @@ public void resetAllManualDeclarations() { this.manualDeclarables.clear(); } - /** - * Return the manually declared AMQP objects. - * @return the manually declared AMQP objects. - * @since 2.4.13 - */ @Override + @Deprecated public Map getManualDeclarables() { - return Collections.unmodifiableMap(this.manualDeclarables); + Map declarables = new HashMap<>(); + this.manualDeclarables.forEach(declarable -> { + if (declarable instanceof Exchange exch) { + declarables.put(exch.getName(), declarable); + } + else if (declarable instanceof Queue queue) { + declarables.put(queue.getName(), declarable); + } + else if (declarable instanceof Binding) { + declarables.put(declarable.toString(), declarable); + } + }); + return declarables; + } + + @Override + public Set getManualDeclarableSet() { + return Collections.unmodifiableSet(this.manualDeclarables); } private void processDeclarables(Collection contextExchanges, Collection contextQueues, diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index efb9b98d9e..9a938ae690 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -1924,8 +1924,7 @@ private void attemptDeclarations(AmqpAdmin admin) { context.getBeansOfType(Queue.class, false, false).values()); Map declarables = context.getBeansOfType(Declarables.class, false, false); declarables.values().forEach(dec -> queues.addAll(dec.getDeclarablesByType(Queue.class))); - admin.getManualDeclarables() - .values() + admin.getManualDeclarableSet() .stream() .filter(Queue.class::isInstance) .map(Queue.class::cast) diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java index b36cc3a04b..c1c19b4945 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -41,6 +41,7 @@ import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeoutException; @@ -401,7 +402,7 @@ void manualDeclarations() { () -> new Binding("thisOneShouldntBeInTheManualDecs", DestinationType.QUEUE, "thisOneShouldntBeInTheManualDecs", "test", null)); applicationContext.refresh(); - Map declarables = TestUtils.getPropertyValue(admin, "manualDeclarables", Map.class); + Set declarables = TestUtils.getPropertyValue(admin, "manualDeclarables", Set.class); assertThat(declarables).hasSize(0); // check the auto-configured Declarables RabbitTemplate template = new RabbitTemplate(cf); @@ -409,19 +410,23 @@ void manualDeclarations() { Object received = template.receiveAndConvert("thisOneShouldntBeInTheManualDecs", 5000); assertThat(received).isEqualTo("foo"); // manual declarations + admin.declareExchange(new DirectExchange("test1", false, true)); admin.declareQueue(new Queue("test1", false, true, true)); admin.declareQueue(new Queue("test2", false, true, true)); - admin.declareExchange(new DirectExchange("ex1", false, true)); - admin.declareBinding(new Binding("test1", DestinationType.QUEUE, "ex1", "test", null)); + admin.declareBinding(new Binding("test1", DestinationType.QUEUE, "test1", "test", null)); admin.deleteQueue("test2"); - template.execute(chan -> chan.queueDelete("test1")); + template.execute(chan -> { + chan.queueDelete("test1"); + chan.exchangeDelete("test1"); + return null; + }); cf.resetConnection(); admin.initialize(); assertThat(admin.getQueueProperties("test1")).isNotNull(); assertThat(admin.getQueueProperties("test2")).isNull(); assertThat(declarables).hasSize(3); // verify the exchange and binding were recovered too - template.convertAndSend("ex1", "test", "foo"); + template.convertAndSend("test1", "test", "foo"); received = template.receiveAndConvert("test1", 5000); assertThat(received).isEqualTo("foo"); admin.resetAllManualDeclarations(); @@ -451,7 +456,7 @@ void manualDeclarationsWithoutApplicationContext() { RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); RabbitAdmin admin = new RabbitAdmin(cf); admin.setRedeclareManualDeclarations(true); - Map declarables = TestUtils.getPropertyValue(admin, "manualDeclarables", Map.class); + Set declarables = TestUtils.getPropertyValue(admin, "manualDeclarables", Set.class); assertThat(declarables).hasSize(0); RabbitTemplate template = new RabbitTemplate(cf); // manual declarations From 270848cace7ca07272a28450c9e520ea07d78ac3 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 27 Jul 2023 11:52:28 -0400 Subject: [PATCH 267/737] Upgrade Gradle, SonarQub, Jacoco, NoHttp Versions --- build.gradle | 28 ++++++++-------- gradle/wrapper/gradle-wrapper.jar | Bin 59536 -> 63375 bytes gradle/wrapper/gradle-wrapper.properties | 7 ++-- gradlew | 30 +++++++++++++----- gradlew.bat | 15 +++++---- .../rabbit/junit/BrokerRunningSupport.java | 2 +- ...tenerAnnotationBeanPostProcessorTests.java | 6 ++-- 7 files changed, 54 insertions(+), 34 deletions(-) diff --git a/build.gradle b/build.gradle index 6ed218813f..c8d25d450f 100644 --- a/build.gradle +++ b/build.gradle @@ -19,9 +19,9 @@ plugins { id 'base' id 'project-report' id 'idea' - id 'org.sonarqube' version '2.8' + id 'org.sonarqube' version '4.3.0.3225' id 'org.ajoberstar.grgit' version '4.0.1' - id 'io.spring.nohttp' version '0.0.4.RELEASE' + id 'io.spring.nohttp' version '0.0.11' id 'io.spring.dependency-management' version '1.0.9.RELEASE' apply false id 'com.jfrog.artifactory' version '4.13.0' apply false id 'org.asciidoctor.jvm.pdf' version '3.3.2' @@ -170,7 +170,7 @@ configure(javaProjects) { subproject -> } jacoco { - toolVersion = '0.8.7' + toolVersion = '0.8.10' } // dependencies that are common across all java projects @@ -285,20 +285,20 @@ configure(javaProjects) { subproject -> destinationFile = file("$buildDir/jacoco.exec") } - if (System.properties['sonar.host.url']) { - finalizedBy jacocoTestReport - } } jacocoTestReport { + onlyIf { System.properties['sonar.host.url'] } + dependsOn test reports { - xml.enabled true - csv.enabled false - html.enabled false - xml.destination file("${buildDir}/reports/jacoco/test/jacocoTestReport.xml") + xml.required = true + csv.required = false + html.required = false } } + rootProject.tasks['sonarqube'].dependsOn jacocoTestReport + task testAll(type: Test, dependsOn: check) gradle.taskGraph.whenReady { graph -> @@ -319,7 +319,7 @@ configure(javaProjects) { subproject -> checkstyle { configDirectory.set(rootProject.file("src/checkstyle")) - toolVersion = '9.0' + toolVersion = '10.8.0' } jar { @@ -749,11 +749,11 @@ task distZip(type: Zip, dependsOn: [docsZip, schemaZip]) { into "${baseDir}" } - from(zipTree(docsZip.archivePath)) { + from(zipTree(docsZip.archiveFile)) { into "${baseDir}/docs" } - from(zipTree(schemaZip.archivePath)) { + from(zipTree(schemaZip.archiveFile)) { into "${baseDir}/schema" } @@ -780,7 +780,7 @@ task depsZip(type: Zip, dependsOn: distZip) { zipTask -> description = "Builds -${archiveClassifier} archive, containing everything " + "in the -${distZip.archiveClassifier} archive plus all dependencies." - from zipTree(distZip.archivePath) + from zipTree(distZip.archiveFile) gradle.taskGraph.whenReady { taskGraph -> if (taskGraph.hasTask(":${zipTask.name}")) { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180f2ae8848c63b8b4dea2cb829da983f2fa..033e24c4cdf41af1ab109bc7f253b2b887023340 100644 GIT binary patch delta 41451 zcmaI7V|1obvn?9iwrv}oj&0kv`Nrwkwr#z!Z6_V8V;h~^-uv9M&-uo<e(-=YK~5`F|ali5r;zZIfBnAd+D~aWODJ zKww}%KtM!5!cW&cso_9C46u_~nb`q;_$!281`HoZ4z93!rX`BHV?dTtLv@i=g(_g}%t0FL^1?zMf4r z&g%|;-;7q>p%uTYrn2V9Wt)mlfvyA=oWUd?77SRLK! zOpdC~&^t`&p09Tb!aJo03f;P4{k|C8ngbtdH3J{&9DCq!LKQ{IO`YJLv^*zc+jLoX zq?p8`l1FQj$4QA(Kw|WOtztkC+RNnMlBoFo?x+u^z9{HhXUzP5YD|HOJyklLJ8Mkt z1NHzv4K#tHu^~7iYGGk!R)^OV9{Ogzxl{=C6OigKjJ)It}g_B`xf+-d-nYxamfwPag4l}*iQpg*pDO)@k9J+ z&z?xBrM?pN5wM2|l^N&f``Gj$%O&I$deTm*MtXL8J}H=jFQ62|N%~VjBwV7)+A#;_|18Bo*}!C?GsHNQCWOJQGs@ua zw%nl8nR8|A*{&E*X`KRK9xx0N-zP7n;$L*P&GaLjgt#rocPw3?8wkOf}~L*C#UfWmwCB7Dst(D z)(jFKE_3`ya9(9^gx}@kG8{DUy|V zsaIU+EzM*ONXWA0>E7a`2LwrVRPbj4rU+&B$*;EEx5(Hg6JjO83d7+`X-x8HR#`zc zg2bsUU!<-KxZF>qL8%62f`l8cxI44#A>kKXkh|t+r=p@G*A`-fJ8`sf5retYHL3e# zTFzg~=I)c&8u&~Ak%uvDs5?>!% z)N>YvOU|WC zOVy}S^KKmQh7yn6>3V(}=n&shsv;3gYbH(goiv3G7E3hlyH2ah#l7e~Ewt7NIFtru z6t1+&m+i3b+>mMeR{lj3no%CfCZY2x)H(N|C`TjQTJzPk-c^Kd7FcXdkl$6kxDzWM|H_s9%#)(-Z(hT*0b#DG}m9m zz4l@;WE>T9TFGV0lgpCyY*%&ss-YlHO?C1+DO}SgCI|9(*59aZ)eGrTfUR7$!4?_c zHoV|JXIST6TAH#fwYiR&Gqyxn zX84riD#M*65_OXZY~~*R#yy_)BE08gv^t9e8F3Praw52sF;_&rp1&1%zypuVfl>sh zMl;{c5HUobSaCP=F)>#^#VDLz)vcG8PF$yCIy8y{y|pqon^DSS>Tb6S#>M83)wP>r z7Jy9592!xtn}e>fZPat49f^zdoJ&gO-(6)(R@ucNk-v>Q9g9{}C(ChE=q>W?X-79$ zITiBIhTP-*20F00g}!8f3i(O9N#y`OQ*Nqqsq4DzF4!(`%BEtcezA2m`z2fs@r-+e zZi-#)zvOAWRLpI0o@TE_A>`o?39JgGPdtPzEX2FHjr>`4RA8IRKP~s#e7(MZLC0zy zVfoC<$ZyeRnf;lV3RbmKE45p9vQRFRR>k^5p6p(LAyaD4{z2rvkU zFaJ|iKI%56!2DYCK*7zsHiMX~NJN+SmpoMS<%-TLUPA7{%qK;&?si2!q5P^6AngW z;8H9K+AH9S9l>su^(;n=b{m)g z3jCG#JJ@s`4m^Dip>2P|YD9iLGP@DJ-H6-4^7VRyhcyMDyh8!SDpphNL{6Dw#1S_z$RdG53l2N%M2ImNb6@5gL)wc= z=!Zo)euXuuIA~VlFjk5)LR&ViZ$;uBmDozS0cM@|z?do@h4Yqv*B<0xL$r>fC5-g$ zMoxGQvU&nqMyP(3pclla7rF9_MkGvC0oHW-;P0^Tz};q<7-4 zXB`c>?*m)YlVfnA)qE|z2Ca-S*4c+d>49q!o3$YqiDCDzIMU2LxT3r{Xz zeBWPCs-;x~rir~pgf@L|>OYcH3w%F>FM;`F9#BkEMr-z3WI;jOy$?XYf*M}Fpf=~r zjy`X&tCs@NJv|CQ_3DnZTdph58cE<4Fh+VIOukBcFQ>w6$CVP@`9j0()ZfHTDj2&dWD*k zX@z8=lDbf7GZZq<21tz^(!bL0I07bV+Hp3q2UqzFQJ13Vz%T{>4l^>^km6Ui-LW{K zplO9WtP-`)FGz2pt0DJ9L3U&ys(iSvNkGURukW6gYqT#_gZ+v9-`w+mNaf}zlQZ)_ zddZ#~;VDSE9K<#ijRp^=673evjux$=3XGC@kYRIGweJA=-<&o1+>`x(QB-y>Tu_W= zd9NriP>kg4UEE~TUF_tIU5aJ~UpoXt4U9@vBs-||Kbcd4VYHM$k9BBZlJ@#a^)G&IP;QF*LFNx?_KStc zn0%JsWyUzqIs~n%wBewA=S)rKIQH`Lv-<{oecfaJAWoy;Ak$D3tq-LdrWjs05f{F8 zMsV7~&LV{+7$QLCk)ZIpQwk21B#7r7#j%;uv=LgLng=8<$J#O2j%Vhe$(}5)hxWEo z+Gdti(MC5VYQ{il$5&+82$^M^yKsGP4x(8`U7~GQBjmvf7PD}`4h+t&cAC_TU+^YD zB>Cvf)=q}gJwp~s&YJ^yo)^_(q*unXr}!@*rJ#0W%4kQ$6lPB_oABI@a0Fl@4j#+m z85Mz9_W&szJU9D|6h!t``>M`S)`5AudV9?h4%iEEO&8Gs#xa+sv{=UM@G5ik<0J>m zKj!Ph1C03E&d%mukH>CPc~Y2RX>{WXAJ1*EFbUly+$YEO7phJI#_Iy<3{G*J4(3r8 z^7S|eCa0z_8m@67I;);BEo_xhkJgOMXQ-aXq5u$VzuV%>!%F1jjDw74W2k0?D+SFV zmP@Ilj<(9PuHUe4^BU5}L+X0y!+&TK2??jw108EieraSHd4MTfV>&|CLb8_NKz&t? zRz*%c^R&_9%UH{$V)$D!<4yXXFz|80+L2GP^X6*YzwIe8n_B}g!xrq*&*Ccon5d~2 z4FY5!)Mm9u%uX4uaVnn>DeZ~!7_pogRCeiLudbwf{t!$y0?*WRyIs|vdTbT~cy=A7 zzw)5;ten0tOvo%n#QFcuXP>UkeFiMlSsjPVx-riyCVDKjcrIPShY1g2!bv{w2Ppbt z>sZ-g@Nq@saX~Z77NwfimXQ1E4Py4|Cd&C+VsCEl%iPG_{Q7*lf)2#p zVks~k{()D#O%Z!WgC}J&*iXSgsLEG{%Z6ERa8jh>5<0`8b#FFPC2intUwy#0O3sAu z;qJT!u*@TMUqX!oL>qf??D*GAC+Iy^LCnz(-COw2q{Y8w$)*)k)(>v8rN=Fbnl1v4 zIdGcV*Zg&b{0{l^l+Ke-+VtGKi;a_Qu3`xyaVbb6iauyB{BrvYn>GEI{+1;cI<`D! z^&O{8iO=ZVm6F>$m{udeGTG8~w26lkDT<*0_$+XIvR&Be7~j=~Y@l5twC==P8du(Y zjlXae8GH{EIWzm%v`*D@{kp9v2-9)XketTu*3Sx%TWV=JmDUgm&EP{C59}wS{O6SY z7k2-!SJF+Bh1B5HnJplSj;V)tXuYF1l6HF*4Xq$vwwIVpp99lI+^1RP2&zDFN0D6t z&j{=hj)?Dmhl;7jC07zJUG+b6h=(E+V!w#-sD4L$XF2HVz598$`gl&IcTaE2`{rX8 z#DEE=Tl&SQjqehgSk-*@*4niygHP|SKLPQL7OGpv<3*m&N z_yao{-B6vPZ{P)J!@Qe4s4JGAx!`X{E4+a!6`~ zhf?C=>LHrouJP1G&%ljUDFM1jMMwF@WTK0ezHrZ7Ud$sY)<;w>5xG)oh3Cy}WIb&mWzwWh1zbth(@w+ zY8A}%tqCYDqpQ+Zz_goUnc7g8Na21&+6*12*D)8-j}UzK;FQdla>2d^wSTbRRI86e zMnG;;N_BkaVanDc6anBAu6o>5DdxmaDI2Z(lY1W%4%Q_5_FA%=WMB>vh_!qY-h2L(U~|#lctsKY|)$M@+u@Fe3~=I+!%`s?v6lPAft> zlKVV-%y!Ov><)l32>62PB?iQ)H8xoW^^!~Mk3goU+q`l;L&VLBk_wz(gY#4cRT``I z;nB4$+j8FS?ErPRUq;F#I5&_3T+ny8cBU_z4mI6Di%U8UzQ-Jod}wqhDOu{NR@#@r z5Bqm=geljBOrBwV!rjug-|$}PAK%fP!_qZmKYNx?TJ;z(&_=Q~0$#-!p@%kGy5xO@ zXJi<@$o(3*a3@UG#lZ~MLIHU;mA&n)=$h% zj|(-|qI9F^cF6wOjl_GtL0`kNPQ(GCB;>JDeWt6J`R_>k{^KJ&_93i`nt3;-1vo;C ze`DCi0Zq4Hg@OoQo$*eryktF#J{KM634!lK9T2)?8JetZ+R&7>$n%`-|5CG-o^ zgxBk&o__~fx(;~aI_RL|cw75V2*wD~37&_~+3I)@;^< z9uccH5;>RO^<>NShBx(02s5p~@)S8bKc7B_GM6%|vbhv@34I8a zXpt75nN(YMkdfB8lx8yKbK12+NAmWU{10^=7#YxL*PF7WLqM$KNOO;?%= z1Pft-1swj7SytiWwxR7pLeh)oOqFb#ZeAzGi;&6{QQUoy?IAdnjSI@U7P za7wOV(|4?SKPX*Zgk!(*a8C?FsMB5#vo}WO6211MgP+o373mfF*abYJ`BMBcKBf~# z(0$l8(Tdxh2wEfR%tPxG9s-EoyAla@7%yT=s6Wn78e8R`nk`I}jnkA( z<{SGJ#Rf6dTIZUb02O@c!Hi(NqvUjPu<3tN)Bd4fVW-HtAWqcDKlOL{xgj>5vIgT3 z#PJanBVreh+LTs2nW288p$x-+?40ZYHDk1o<$yk?!?D22kmjrK_r_rOZ~nY~ut}TV zTewr@bdR=jkc3Wo{w`U(;TS-;yV#tkU%-SEF3flh*z>vx)cCI9qYTNWND=m10~puB1Vahw6Hm`fo9Sy z29$Ch)+WbD3^(eUjP_J*r0N_ZXJo*C6n705LQPEEX#jN@0$g%GM|n(JFyK!3mf#x- zS+cvm%10KDZ$H^^$Jc##d*^27>~(X4)PDN8!zh5u^akzJ}R|0tBu3=h+8GH-O`&ZGVdnofbbogouNoVAS5mfs` zn+dlKlIQ`=Ki1nxoVLxy{BaNJepyCBiV2`c5{RJDy6VlWPzuN|_QLnbp;$3p+ad{f z@fA_3`b|!*GueyTN_R*!QCjdYU8TO@ftUR$vs39dTYT2}=C8~IXB_C*)CO$p3~_9E z1QkEAi`DX|j09zF?597$hVs=y=j-ybnGSSeJeYS2J*ac-hLc)Vk zf1+B#~vWmi@hYlZ8tuDSv{O*Z;^?O@Nt zvuzg_X3-`1PL!^Ps%0Q-nhj`%cJmDRW2UI0(|2ib<3z!mvy5BH#(YfU%IK-o&JA5! zgy6d`2T+jCr(Hm5`Z>ssmX~^))1NNW!+I#eYL7Sqqa1$DW|E* z<;{JwUOG0>+;`x3xf1}%d=S|G8%cE~B7D0Cm(^X(b=i0mj}^`5=eG5R%_mw}HYI_Y z6AUx$>8J!GGkMt_<}iSQ082|BmAF1MMZ}}jqW=^h- z)ruR8Q^E&$P8yB8SUq(^lw3GQqBTNG>5Iu@w^+~F7Dmiv-nUy-w#Xe@ z2nW9WHcS|$I}h&CUBjx2Mcr{$BC~7=X~Wyl8kyq6k6$$t!yNvw$HQDEW-dK^iah$@ zB|q?%2?CN5q?fYqMUWRGL=-8SZji#JcN}yp_Zgwe54QjUS3P|2)05;0ziN@S$upyB zdi2&8y`Dr$4VjeRmDR%Ou9I4-4kXTImg##kf0uHr(ueiSw=fONz${4-X3$)Td8Y-4 zr7m=H&?yvC_+XX(El0%@)ow9H*ta^wDY06@AzOeHV-P+*d!>wxCILObOo>caqD3<8 z^}^&lfWZPpuPMWT-sN*n*c;x`j9TbZ{iX#|C~q0 zi3){=St>6WmWB!q)O;G-25J{?ncT^QJ&Q=SRBN9KT4bqo8Xr(N;KMbD|xw1*y>Nj!ehX*mUp8W6rlA z?Na&>cus=Va109u4b6JNQ1yX(oS!@GX~IQp=oe^nN2%;wRV3hdOOtqm(?yy8}vffp-nCH(Tce?$%klfDkN`0 z)BY`Btm4iSYt#=?xO{Abr|u4GQ&e)vh(RX8(s}<@Zhm25nt~&!=V0(6p|A1jQI?Gd590g!PI8f7;wuBJaTiNNL@F6&FCs8#>>eBz%(pXT7Wz1U)DL0|9x2`rrR;eTVpf+*EzVB_oWnZ%h2` zRZLvEU-fcg8}Lm*FfcYnuV{y2=m=C^PyJciOM;a4mPe!bj*nelq>(=l!if8k%>@*7 z{{&Kom`i)kF1ZGrv|i=+^#y=u3?#*2!0|28lxfq^x~oV+aj$HoBuz@oQL~E9=P>TN zn4z`9gfN4@r8)@$mh_*(9MNJdRkE&|7zO4YVVc#)YSS<3DmE;fBTh$Zp9#g&tth^kA&}{x(ovQAga*z#k z|EULbPu)$-4h@hb`cdQg!!7W6^=}NhCP4==ovTqVGXL?8;Pe29wq#qTKpJPAprMwm zN!o2&d8Fq{CQ=*Ob7K+HQs~_w5am(5{LCRsk)f4xNYbuMjU54jq?)N6@s!8h2#Fl( zPovQu851rL5VAml1?$?wh-!RK@t1Nsr#mRL@%oBHj=+@1xL7rSpmt=zi3l4E z$x(XPd-jeO{1F>K(i`2oc*N9l6XBE(rpLr#xBpI_ljN3W!eIE1#`I!SW@s4AvU=mZ zcQB5*!Dl%fXAG^ta1x)QM!EVu^!azXlru{$tbtgDhLbYE=MA>P-2Y-cG#+~X!5@*d zVN=~8(qnuma+vBy$Q>L-1vV$Jh7dzKFjUzuRl%$UDXO$v4_DV9R0guKEc~BfjxYc- zuKEY&VW?!|bn4{(8mMIEBdp}vLRb=@^8t_|g-dU;G^GT)+R!v|g+6ah}V5R_lsz24(oKmqnMQH=frr> z`($${^OZ{FCfKueD^B_{uTgr$djyPJI>(fbhLS4)KV~MA==nsOCGYbj5_Yf7#39kh zllvyuh)qaWois44pJAyd^He`s{;SO-iL%=tVQ60L4ihlris-QBN~x&j;ctDvNVsySl91|k>MJ)Xsz}&eP6JNHWn0>x#+IyubMbFEq%(=#3UDByACnZh@OW~d~ zniB^I$XRqoAENu?zBL#eB~B=-Wsw0tZFU@H8O13JG^kX+8$t-_*;2XU8hX6rdASfr zT;`Xb5NYNH4Cb-P{gt&>-!jKR&U<*Y^NlM`^PN9VEEp)SyVJQEz*oFwb8MOJlhf$P zu9D5go2^z~a$j=w?i|T9-IC>P)crpGB5DV4UFh3TpWq>m(vm-RET4(u4Ho1$l4Pc! zE9S9a;1z+ghz1Ql$t6|KED%HAC zBsQfDhj?`mWylrgnq_{OK-JOQf14U*p|I*aP`DK9N1r%H{qi z;yAikGF!SBo7pAjmzYELjaB5wG{csLfc;-$OD03#VRBZv8#szTZZm3y7bx=o5n^~5 zs4pv%Gb*J3SE+|qwx}rL;tY#KjFPB;V5=HdR1NuDl7m=@_mX-i8B%icE&i%nqw;0uZ+!qOin@ZTp_6Mrgalu}r@Z3UJZYea+> zp_r5YNdnTFoN#Wf-3F45hVY4ccxMCtq)qj7wqgMw<1`J8q+Vyn?*vt_2pR-i-3hA?xbe2CBhehaQGSbDn+b6yRBbN6Q`>cZUcfmjGU_S_sa`6c3+-WByPRRZK(WMCM|WQio; z+h-2>{5ffoZ#dsgO%C*1V71($`hngcrZ2!QER}Z%mF}<<)ZASg>UtG@b&~9*5m6dn z%SFODi``_c0cxG`B5Isq%FB1WhV zwbyTq&BxJ#V{F-R_Gr&aK;Nbf_I>EI{Ju_=FcDh`RL)%5W#r*T7Q+3uX&mjd84O#u z(depF$`7Lck!P|4K?ViXr7Fz%1j)z6=v}-(t zNy`i9=}-8^<`AtiZr4L?D@D2hm@FaLkA2ea_}pCLtI0Te+4orswjEn-YCxC)m zgUf3D3kBn5=CLZ6nk;-R2cwAR#uZ<3s&^8zF==qqaW=DnlbMG1eC$(zN~0D-_(Juv zNyhoN;yk4@Lp$cRbAIUW@y~twZg8;F}r=uQyr=~US=tqUof+9g8-h}XO$F3 zYi1^}!Pq2`<_T%837-`Uiv5WWjG+Ck=_EXOa!1m%1XS?Ixu>PWVEwrh8fpn;l|?3l z^NsYMc&$MgC4l^gS0Drk2-|aX9qw;p{fEC%o zaHyRuOV|1~JV%YJx9yIH#CJ0Hj@3b!a6hrRfa4SuK7~~Bv)?1{ocFBv<}M)M3(P4n zEtaE-i><=qZdd|Qk?~Ti0-cRn@JzfOrqbsy)W{>aP*&^8XHl>l=SBZX##Pt7MXRA;tt0~t+sKh$uhK09}CP8SIo1phVM*SsazQB%^0 zPEi%jY&u7DIMch*8<&!z;`l^tsX?6{UnU{gF>IHkN3!DyYM>o z4KUsji$W0^sxQv%a@VYB>n^Vx0ItJo0{oFN3G+yACimQ;FWeEvQ7wVaI_2du_Je@q zMKPCMw>1usJqLwjHvvHZ6Dpgj-$C2|pkn*487chVP>KFSluX*h3tNkC z2+!@Xb&B0=+LRCWe~k(kz4u-lqJe;%(Iz{MVI~(8q9zNp!T`LD)K)sa{U@fkCT1Xi zlJwI|jgxJJ(4Y?DVR6cU;Xw?MDI{f^jkBOzQ2pGh2zIX=S*;Crr>!k(vw`FcR6e)8 zP_eCU6RPdiFx-6clhv%X$JBo3f0>oDNQ#d9YkJN5l5^vCq6;|T_cRdtdNc-MKdvNb zIaEBqvwV7ujsy7k73_-=I`|bF*1t-f-0pIG>JJIK+))Xw79OG#^70hzs}c@5b6}4- z31ELX1tSMh6`4kuc~k0+(KuTltg>nd7%VJzX$rbvgw++xy7ZV-BpRQy>cz&~$`F|+ zCK^nvnWe;8zXtM8S;@n>VH|+h#~9O_u9)WN?5oDBVgN!^F?a9ISw$wSYqK+=hu9*K z3D$<|i&Yes%$njh*u;}7v*eaoH5JyBDVH$K3#r8UuomG|YKnDc)MO&5O8L_0!W}0l z>QffzRO&3~y4ggpT*5Uis-ETaXOpz6G%F`II<#n;d)OqC=~i;9J#tS{-((&k4YVtE zu&q^UO#zJFQzitKifQxkGR>`Q3dyAg+GT3|l4IsBb?5(_@yrVz+&g}xU8vBz8)%Cd zpQ343PKCK7YM!qg(aAGm;c)IZ;Oe8n4VzfVu~>*p3gE!5jTH|#T_lbFiTlBU5--N7 z&6v?bfx>P($jVLtKN^yr{WlWA`}zFQ-4^1I34qidL9RRWd^Guk!$RWXFbG&VLAiAo zoIK45Bf*DIkBPAiWy=F{A?wc>>j+ZI?g*_#bB_zA=SYJJvd|5 zux=MAHWP4|RilVo;A2Z-V{zFfl90{nM9VGLo@TThm0E41v20&cU8mpXZ2nZGKE+gp z4tPy-gwrFcIE{f8#Z+!y+0tlaLn&9=?+8Xk)m6jv4SdCh>D&RHK;0O!GgxyYq9x7wJ+=4vfWkZ1zZ(D_G&zymE zg-tP+)IP-hI+(7gq~j}E-CQ(cn8#tW28hjd8}Z;6l8iGkn79Gc#Iocmg*~e-wzjM! zG--c|eBDc_lC{l?WvGD+g&#Pno+zBy%v9Yr`UI=!x}ub*d)JgO5cGgea&L&Sg=5ijf7HtnBxOX6o<+CaS)kV-;gg z_oWq%HlSxG%Kv45YhI#GysE4y0QA3sYYnr3mhZ&44rFGMKZJwP;$1IL6p)4BjWEYS z>YOPWc1l+9^Wn^UprJCwNI|*9#ffFlSg~1NDpTr7F55NgB@j%=qC0rAlpW1DaCiMe zONaiMyR~c|eyIG^JM93^M(SF{S)(D&cSwgtNNF~B7r1V>??x5vnlw~`3&0F zLT}s12H%8GecxPQO)7s@J*6;n&0TgH1dOdTLkV*etXeNtNGDT4_^y>nC4h3*v&1eW zNzs^bX@l-zEFqB`Q=QX0mXohXjmn!9-Ogskl=>|Kkl!gR%484~O)X`kU1oux_>659 z%N~s9fpY>uA2_r08fn_6fSSZCf+CfC{!-PR4@X08OXx^wWPongV@(u&yvly;ME|p&b79iy=BV+xw>*jk@TXuU>RWIsW z5~1gt2i-qvVmGZ!@D|Bxp{_^$!M=?e_yeJrMiaPTU7$Bgh^~Ss0V47EW9JIBNY+go z2@PThX9G_bOpT5ecdb1u1 zAp(nFg&{fhGoDoqCxdgvPTmrRxhaqsL+Ye{!g zGDvrmpeq+R0Q5LSCf%c-0j>QB4yn_oIm+tEj`Z&l+P)>2x?(e{KYoqaoLJDM(3NP5 zZAd&T=3`}FBdhc&EhBJvzGZt?Ma=whp&ao{5$&@bC#O5BN`n~Om)at>a!{zSuP-$Q zlh%FDw#(8IK#BcmhdQ+XIx}CILfi_(=k#7q7(4RK0tnQhIYt|8qwxL?cZ>=>1odG= zIk$ojtyJJxKXSAwj>uwwUZC8Xvf)x-{+?cL7?Ml&55Lq5j$zj8yRCX6)YOO=e>r!r zG}stL91#x}AXQwf2$5in{typAL-bM3XQzoy-rk5v(w^n^8JL~}AmhPptCK@?juK^H0b)QcNiy9)3KR{{yBQ~{dgrwB&aYHl zZ!LJ;ziTR;DtXnZ8zQy2-SeDFCOksG+Cbr)8fqFI^6oB|eP$HTwuseWVXX3CO%18> zlvg&aii81jm&ABhZ0|;Ck31CM#(E}Jqn9YhjeFn=*xxf+`G=`v)f8Y+)9>iL_=dB=^X-a`>(cNWQi=rEg!(U!a|j&QGLh}lR?0eA?H zzdq&#*H*auUz@gsmKyY^r*miGay6x|{f_>_=Ts+ukDoXy|F`z%xD}V_K*dH*XL%*W z%~9y;@M#Ov@BG9iBmlu4M@unLAbxp8ReBGDJATBTtj0IimltdMdwUg^V@{{&y+4k% zm+r}fM=#?KF5es`ArMVx<}F0%J%Bfy_D4;s=WS&(q{Tqk1~6H0sBBFC6>rnlyKz?@ zZp2ndS3Fx)&jm#XxjVi*!>dMoiUG>ht_T8rWi!N==iB{R-|pu4#$iixV4UN_QjIm; zPOoR&`ZR1u>64-fiB!`GWE2#k`fB7h{6K{_5Y?SBB4G?abn1jJG%Oo$QZHm9V=kdRb6cO|_b z|2v-6SLw%jWywy+mVsO`JwV}GC_SNKvUvH~8_C!Q>q=1K?w-PR3|X<%|Q-dj!C>kmnmC$4dCx5p^ZFCw`$wczAl9+@L}MdmTIl(C&)8y%=MB6!cmX4DS!UjWsP?e| z2o7l6x5ARdP_Y`RD^Jk>^b*GSExzw4FG|W-81A(EZ+yncnO}QrzyCl-AdDzG3|QGU z+V}+Lh-74850KX1*q71tDDCRk-}^nK#^f|tbDu)xdOyuTFsQAq)x0zV1hhY*Siqi7 z+Mx`tH$gzD)0xp-4Qy;v?=W9SA5T1@Sz$BVvn2w#L+mO2JxNVX5&e78dNuF!#3!i9 zg!gCQ-}nPVjzoA>wL0^HX&9c^(DNjiIThaLiM+$f0X8SJPPs-jJ{&E!UK&HjLScVi zaa7~07W^ey@}hecD;bl`gy*hchVDI>Ex1z%`UwskFz>t^!1rBuK&R{JWkLV7Pzo4* z8WY-d)sE?!rO70GM^qEE^~8VCAAb5!0Qlm5!Z8dykP3emkG8$Oi(~KT&NkHn9_I?{>f$zx|Ma ze!N0|QJBUx9@+isK7&7xpXrN5bGce&0F;%I;^CBMVk@#zRhU4`adiSQ{nG5lqO=+u zUzLz z=tRl$8(wj1FvD&=J!;JMmkeB`%P&x&QAJdC09COCmQcl zTf))RdR+aRL+#H*a!DM%u{-dEJJEylhl8PLHX`N;vQMqFLv!t*e3U7JM8em~tq{#) zfO|KS4ll zsYzUqe*9a~PS9@dW<)1^rc-AvI0<`yLKxtEM_Qv;U(CX&EUDf>eJP?qD{3Mv&9$|e9$3PQ{?dUw$PJ7B9nr-;79FYF{Omug}trfa!!Wtm?_nSV< zv9tzhcK}eq9(D3o4+PV=(SKJlUN@=xt0)^Ue$+t!H>T+nFr^{Qid1KcQ)ygF5N3fJ zBvJhx>at!wd-LmMduwg6!OfB@ ztFio`CLBnK-xmr8qtC)kQoZkfbu6p%SJ7-xk5i?Z4Jg^wH`e%#do}u9k=yYKxC0gd#E=04>@OJg)zPa@9{Oi{gf1m97tVoZuy(W^O9~A$)v(>CWh5++# zBgkfs9Q>b&TU`3D{UDR&c~J2GwHA+$@_&n2=FIMH)^^O`|FeMv!2SQYwsvqccX2TO zAHV+@6D6J{lk567PagSCBxC>od#GgWW~Jt0>|yTWYHTNJWo~L~?!shhXYA^ls-~-n zua5B*4q*W!%B%`#grt-336k5y^%0RRY{^imEu-c7Q7Wz<;gpr*!G=DU6DaU@kWT{W zPZz2{rj<>9zm9k5n4>7Qjzy-j&7Io$xV+hHf4jIb{04D?+%=nzpTdnfjEbzrs>{rn z*%S3k5rJEKvYs78?3vTmn)l#lWH|p|^zX1Yo){c^&ua%bjSV)1bzuoj?5S?y4_m(K zRl{LjXVc)}XrUA;MMJ49b-06{`L)a-5-|Qsz{YQ7WYXNw_<>fAlB(S>TQdI=$5LBG z#(kOiCiFnLhbqBM$iUfZrX)JqvqS@Au+`!$dds zlaw;hNZg`tB2+e(5i1N5K@~>Z_h`YV)+YOqqqP}l>!atGwW`Mvj1}#Sh*gTjGsJEr zQIR#qsT`*7z`L2ntA_8x2^*0>VOSaIj$QJa8|47FKv5a0_F_YH4+c|eTQ7T6r1jB1 z_+%GzyEElYM)AmkXs4|hTV^t7jv&n?m2OQ*u<244Y3Kewe4SH}?@-(2yHDG;ZQHhO z+cy5EZQHi{v~AnwX&a}F>2JQNnR(}8s*+0OA{VLbtn56`Z>=p9*Z8n;5maM=+7to7 zu6`R5>Tg*T90d-$J5qUUXuIKVrK$l*SHVcU&1V!BG&r?ipAu-tkLWlliU++1cBrCvCo8lw3(?W?U_rQh;`V*y3crnygq{b`r+J}!$SJqV#c|#N`%%3W06rOA z|IBj>apbv+$ZV%E`j?6j?3B3?BE^!(RBf{pVk9*o9Kg=F<2&@px}sbIzdbpfa}={@ zyS{lmIuvg$0E6ofd@O!O&?-l)k~D#Ec^@H%MCt8NIKrP;Mv1T;a@&z2 zZMldhP2M4A5t0I`Rmpb29QY-FK%SsUnyv#7wcHng%#cLLv10l0bTUpLk$m!8clrEI z>fKX?DVo77ux2f)%JyRJN={xY>S!%t>HB~14sp!XD!!kRI>b-+h5!Gj2^!8uj*e!| zqE;@h&Q``hI^8W$+Sv4r$LKs1nX!sSEE+>eEjxde$<~7RP|QwQ`@vrthUyW=1V~y*{pO> zEMHu1#0P|i8ofBvvemnA71`|(2%h(#xHmJ*0MplpVTZmGaCo_{SU)WnFc3$rIMqu! zlf*WiVIJ36xvU4W$gXrwjQPzc<4NV)NQZ=u#>1+7viwbWv@WQ03o@ijM8n|NV{ZE- z)80;ulFro_cE%KE5C=S!HdFX!KB@wcViYEB2Oq{6|3+%) z;?$^>(#a0)qP??LM;M<~R*mI!vJ&r4A}jzV*~qdx{TVX5>3;5Ec(}I(^v~FwOTEFb zDfq-wL@9hHab7)s;CJM#un72}39D#CHy?P+VYvgWXrt^d+gpp`cv5{%F=L-Q(DCUK z6Vu`zlMmFhE9M*s`8`~dTg$WXu0*DL%wZsw;H016he8;qR9^%rl(AtmbVrz0Di`pi zHW9!t4=EnVCls%+VyZ-C(_V>_v$pH^;EgI?gb(olZ20unFI03SF#<~h1a&5gf?MWD z5&%YEH3m&YVlZ$FUFs5PX@yG(%v~LXF%n;%ptXv^2}CI891PifEjV;`InIaincN zH(P)$>iM$)>vQ#-oMBB<|HP0i9gV9& z{Y?S|`sr(pqDBnXGK1o**tqsDL8`Hf@Itd)Dfg|7z!;*F$hR6AU^}CIZtiTIn9#T# zGy}n06W5K1aI2W_w?6`Q4oL37%dQAUS$pZMXe81u1bbr8Ory)TP8x9us3T+9gfX#W zh^_76WCjM%;=wqkUDQ0R{3hr9qM(nt3nJ%9lmk?c*o^X!Ckugwu?-IOGe>{d|E=${CW3BWcSam9*ZqR4qsF%9fCvR~K z(HBhCaJt3$&&N37OyLIw1_T8Ali5R;goKQqBoB-V_;1CCQPfD(3ivS*m}yR8xE?*Y|TztZVc2dHRh zJZDIeLf!qc$;nvv$?NX@y!!JzF7W;Nh1o~-K}zzwI6A3~(uh4=2AO^`eXt9b0G+gp z4nRak5-o|Ww zx}tuf=Hk3kK2dREs`9PT+UlT__>t!V8}J%lB1@AureiIC65*4oP3uhK)X$2ySr8|t z#HEj+KSV6(P>dW!#XyJ@$!nXEvc;`xl$?Or`>rKi3z_t1aKE4 zZkl6ow%DFxdR)TP^p!i&qS!whyVvA%(ix`q%89WrlQ19a32K|z(Nm2=WASolnT(1x z$8HIBqn^$*|Ep|0K33~8DOby|(WgU#64_%R|B9=-?vP)jzeGD=r>%p^Y?oS1iy>`X zp+z(r0s;ntsmd6`5fRv}n<^bz1VDTF@t^#W`cr&D9C{||N<6raWRW95-+2_F+8~BL z!yv|5L_K4Ls-i;&g^;jM`#JzMnDPZRZ=MV71Q1YeM_Ca# z>try10o^mCf!w2h3kP21Nd2L5f%HlI*b3b<2m-cy2+Xz&^R%=V97u3WGI$GPpKre* zqNP$I5`!l`Xf)jfP3?BEe){!QWhYgKyPTx4TOyHliB^N>IE5qgfXabgWjFL%@#Z|O zL96mkt1{pPvcDYYaonD?18Bt4FL!vgtZuk?(#~zsRiU$2>}fc6trYj3pihv1b68!a zt6dO09ZRL%FMr$C!dOXyzGe4Flmk~$c*NS@aP_W}EiEu#V$V<~Za%N)e$H0*_A#Et zw%S%$oR64~hI^oQ;ABcUyvs&WL7MNYX^~Lou)B`=p)b2wU|C^10ml|qDGm!C_1ijE z=pvowtI@6OIj+Wk+B(j+v8;un`JB{-u}ewyb}7#AF#!CGOmCKWg>|5OSbQKPn})p$ zGBEn3&C6(^OtMu6ScH+7d|2X)(&|ka|3nG_`KY@>lPL|o^W888H{?IhlD&S*|}Ll0k6n?0INRPww>!ftUgJie%;*R z*$&~hRw8KsmspvNjBjay6BQAu2oAJd)=J#0ziN!if_rp0 z4N~wsi{j_%JqQ?kOwX^VQzmu&h?pj_B+Y$et*l`{Q|n>?^#ah z8>Kt#Yr-@iieI*BLmzR&txh zTWZcZY77#KjJa2-T{AtR>eXddc$*I&XW-3lZ5-&AQpRY7I)-d^S-)lPPe?&nY~zi( zPfzg8)_8ZR(`d8h@htq?N*!&bYt^^O-Ph0H;Rm5X{9>DI_`renP_{tyq!^n=3pJdn zL0oMqJi6`t2bgxdrpvluzZ#0NUvJWookjk}$r1t#Rx-g(-G`ZPKPf~_8KUB5y0mCj zIXSoaqu1?!hl^K8sbjY!I=ubUwjXq@>>8L$pyp?8osJ&-ocb&gcK6q}T$qv;12qiq zu`&-&)(Z=K6T4RZgqyhJ4f4m-6^%v|e!UB9UslXU1?c7sDyOUIJ3o*^sj5I|<{WjL zBph93LY;tkPEMnupX4ULraH94H=GturCzYRjqBJm)2DPUd-yH#7h~}hdbX9MZE?T; zrR!&Q#M2P*N!sT&v`v|4eo(CmGi*Nh8Fsj#6Gc+^&PA#75`-VPMFKxClPNO$#+X7sieFzqQK0Lr+IM;%j=_Vgx+C z&?h%FM(xR*u?d<`sQv~+GNsnmgj6am2nvBhcue}j9H{TIM?p>-PZ5Nl%k=&e@Qfn- z2mmt&*UA0-{q)G7_!XLqe+hdnRC2fOB5KKki0}z*rKoz+8JI$>^-qLE z9Z6IZ6>23GAAJ;3#yH!}IMYqa-D*L`QqG;FEjrnhBS@(c{I9iaE>2l9G#S!GzMXdu zcCrBn<%x;6x1l8h9n=gu(&EQhOUlZ7GaxL^wT}VrfqbV>GkVvpyA$0I`LbHJf65V76{SIG6(vY{_|D2Ga$EpS9{#1he zf7FEaf2s*DJPzR90Yw7w>&e#n$xJR9M^Xh_G76?8X$`&v0a?GFDtW~#UPB6nGV5W2 z%e&iU_9XN}%+1C|QqqmyOUz{OID{s5)s4wV z>@SXugMHk~3;?aAaUi&8`+=iE=>gl7+7 zdtg6;Ap;v!5j*yXEYh}@N-Cl-@BG*Gs*3H5E}Sh(9a(G@^(pa|wuA$HkQ#I&>)+OU z7780c55V1H2EF?a^961+LBAh;xsp`2XK{YK8=ms|tMSod+;{Mww46`s1!J~!N!0TY z2l0udK&wDF?-2FG%}k3laeYIG%FOh8?ol!nDpCARX6-a0+-;Pa)ZOT@pOHTo8MRO? zH@{iiRz`0uRxJ3V_49)@rWvOfP-cI+hfOY39Y8DcY;8te-K_5pJl3Tv)}IWGtCX?G zB=wN|nDe+H-z32VD(|dqxFLEL>urOjSNqRp;v=Vey>ytFhssG?4eX7hZ$KyPox4cr z;5xg$&?~MY9DD#9TFtlxSQI+D0q z4xqrsp*Bf0j!ue3@k-4ZD)PsLr8N~xzPEC>lrrLcxSg1eO+t(&_+Xk_q_u;xQ*Ns> z$ieR65Kpu44Q4TU+1i;^0WOnoFK5l&;DL@u;#s7d5qdv9D`9D;=vjQxlF#kg*Q2b$ zm`=XJIvEvd_0rM&Dwp*WJuHUopw9#50x*zu%iAD@phDfPoL{<2k)0y!4HfhV|2(fV zecE=E- zXiywtRcm$*35XNf9U!i#g6~fEg1mwd#V6_L0y8O(y#_5Sh+T{0tlo$6E)C5y2G|m4 zcA>HJ-NSSsOOz6jH1P5&kJ{**`U&>q`yrAR2lB}opzdLXykzqe_AYXqB4m%|X_f%p z9DvDw#6W++C5|ihRhBusNElTry=$EYqo0zfxp(z(4`0N=kyg>d27 zFw;qc;tyz8n!s8F070AKp(JSuFOgfWCTszex?$9L$-&xmUF(U+{yXtP^|e268DMGvgQj~nzqXuk?wztOQN(}TT91X*R@kI@kSLqjb{hR<5h2 zp9P+~*Atl*Zr=TS{ROYLj<$SSzPV0zpcFnX`okhDvA(<0soR$Z&2+DY>Hxx-(pFvA zv-j~?7Cxs!xhex{zS>ZDC+!O_thpM(;Ca^tptET^fymq=Fl_uH=H`14G^$eS-n_o|+!vkR7pw>W?U?q+u znrmhvS&5fGS^I{}bc&9|&zRtEj2h*TaK~MIXlKMkLKx#?pR~1RiL6_B#pXJM!#1e8!*TOZnpr*TGB~+#>k+d3XTJZ2p0iu|u<7dG zIm2=O0iWZr@cP@fOAB!5VeJc(G>-+ZGv5-A6{W>g%Ce$0Xiki3fU%AOFE&*$JwGP7 z6gk`x*w6+hOwLgHbgh#W9;dzU={OfH!Kk&f`=`NTaU~Z|XTt|1C(B!K#Vw?L(-t;k zKVd|W7aKN?l_jM`Y@neHE7pNY1WM*4NH%Jvxz1p7ce%BwtQ+8PQMwbu^Tyq|$?@;` z>h${Z{2aEa)$Uvi!|-56xX^_fNzhP22jVX! z>N8WjNJ0V<(#vD5q-(JgsWp5^^$4Gmi|yL*LLc(KuzNvxJE{$q>gye1>Ec_n6E zsi$9$i~fu}XFGtn1>uva`Zps!>P70NxYSTk!HB&JuIO;Up5$6IMIqu|;MVf;A|0n+ zDVRlaNX<*Gq^rhHGcc0$K+(^Q5jVP(u~~gK|GfgY*A*tIijLVd;j7mHOyXUbA>WTu#gA3L}d#+(`5)c$=}dWEVmhaG?kbrb3YvPkPE@0QBM0y8aB z=9Lf07<)Zv(fP?mk!GBb37%uYd+}2FQ8xGp1Uw)?=^XvXf!8em*!RTZ-FtC{rn=wr z;SuZ3&5F<-{(6AlEpy~%;bj}UH>~1)36N5{t+2NSQ^IPk*AAaBQTHq#WDfJLStYN={8h04%$}%g z0&pnHNpwVQnVPM39XdSx?A9x1Fbal-2Mu1AmT+NQSZlBFyUAy}u0|s-6f&LJDpGSzB=0iw%}SriN?HH3}ptytDU{h|}gE(9!J8nHgt9xXxdi5}Chq1tg2733>Uj~7m~ z#1QKyh@Gl43Y|1nxqBwy1Sb$kybK0#ip<;_ ztFM;#CKyB)d&3|F9?!f}?3jhvKIds7Ku=|Sb-#tQLc~!o?zhN@@%3q4NH0HI3W3&9= z3+kN}V0;PtAR%9P83h*rdw);>CAR1irW0~4jtw>Y9Go9ZC$J*6>!d}&T$dW%%ZvQs zE~aP2j>}4(qgiJQ1GHy}c+Dv>>fgPGMLyW=#rN^KQop1a_EYi+0k*dfA26k#I;&4V zS=+joH*pc+cz%9apOq6YE|bv$ffA9su!FozHi#I6fYL?lcaBBG`KIKAr00x z79v)-uW6!5RYs`MK%jJi1anMF!3iYy1j}3LvhsF2BG@;&Zp&MSR}Jv*OyF1O`3|84 z36UylE%5J913^kt05npdtMm-VOY6tan7izbTHu+u6kft;2z+&AGHz* zlfrvMKMAgg-#PN(6L>d|^?@G!I+)NVkcvqV_j@{?z~wz2aPf)L;yQS(&*Q?znK3DE zrPiL5l><%D@M#hBJ#d*^9l9+K%#xj;l0I>@uW2qGHH3ZXeI~?Uv7t(K`8eD#GRy?{ zccA&_t(CRP=GNfVVGPU*+3uv{F~zN4*6W40m{hfWA^QO2isewgt?!H2(!EE*(mvR| zBvk%f$}ZCxpcPE^ushuNV9XiC3&ZVKG(U#_-p)sHgGP?wbG`{e8O}I{?rrbp-aYGe zd~q%l^t)t-VqG=edWI>ba6ZTiMC~ZP43Ueu}dTfF2i#hIX7pUjGVerKkLHsJhLiNNCGcPs8PO2}|rQ{FjZG+l-6IPN3OuVNRTchtj#fyjj$Ji(WIO z*Viuh-AH|lSwwEPEesEOD>MK#jfgAwi^sHpTI+vrE7^%bpe)3^%nn*$ZSsQBv{<2z z|H>YJ1Dvk8r&UfLUfifZeI;23ou{YHOHFjFs*P8o2VC5#rFdu_Lfzx8u%X3GG8~W_ z4GwyybZ2DLRwj~JrV5&E5Yqdaw4Nf(3d1ZMlmr|LH3C_NhwmB$gm+ zR*mcoC{M&b8R_ApStrB1nA-Z2Zv7qO2>Php3Wx!+wj^F*kNk6#tfjJJkHcP0xP~Hj z{mYlot?XCgjoHJm->8v<2xxGj#7tiH{on!pN;1@Gq5cx0J+lcr=Bg@X7Lit@E_#Xe z#pY@Go&46Z(G?JTFH}MTv#o>Qf9wkLaP}=ija7_U!7l7t>MN7OryKE#$+PUz|1M$g zx&oL_$lypKXtEE{dxiH(FvE>hGEC)i`8Lb0#`(=Qo}pa~?BPZrLp>zi`vFz=m?rO2 zMtSll#`Pqt(|4rt z@8{)Qfj+Y$%75=f+|ju*^7&&-7kOC3>;i)BeEy8ICTid>o%$Tf)$rdq^54;_=Xosh zONP=5r|*(Q_R1riet=4 z%vCjvpA|ha{nMFrCIhd^g;dV>CYCNRcFh~K3}#K-bx}8G$~?@>FRZ9mR|cz5t_IAs zaujQ+mHy9-ySkX-t=)0L=#6uwFBztR_`sfD=sEyup~E_{n4rwK^t$8luTi=a7dM$~ zT>QL>4j@}PBT+R}o@?>YlMkPG5Z~~LQw?=tMIL)~dx=bxT<+>sU~3>N5cDEYeE=~ft#T$d{O9&u-5AovqI5MN46h-#uZ#S9 zIo~|6i>+D0N?q<#DO2yz1+RpQt*5YPh>fD$I={s}P@YHH#pbvzE)ikqfjN<(z_ zmeN*Mxey`D4N_v0ZD*CL=McRogMyK&Oe*a$6C(55r?2D@f&+k1lq1c%);@QU(hVBb z-RH*51d{6Q>BY(?s>{wzB!yYJCE1vtle^lAV|Zp=q!1)L_?WOBtw%TXY}` z7!*<9;XbPr;6cni50Ez7)*ub(Cri1taEKVPg!*&l&^wDSoUOwDcDev<{`a!w=mSWabZX)3kU~j&Zz8Rc;p*aB z)?$$FZL276%*0>Fa>a6^eMYULq|#S9r@23qI>*)2*LB3ubB|^xwK}X(^$t{r^Xw!q ze>YVd&p!A(j!djn{->p_#+~Io5{a-Q1fI(Kj<6&Cu$0|ghA-A|hR-E6SOvRno?uoth^ik%LmV}ouB}~M+oo&xhmxdI-q-bcTc{@Q3|mCbqY@y zumx*~5BjZh_P+Osiaft4$vI1c)cw1vFX|Cr(wSy;&TdPx_2JbOqOO?7i*0)vg9r;M za0jMX^5BhqX0eY;Xt&_zmx&VS#IvJ26Dic;V-QfYSm!CkqX>0?YSAOfqjbS22GL+# zKm!1y0;&HlxmZF!Nto0do_PKbl2#wxBgz{UnlQNg6S@=2XS6>&Ov)WIny5V*w}SFG zsb-3`YQ;QzOzz4lR!r{lDOYe3wT6>1#()!Y0V4_;P34~|a-y!(H2Bs$snhg7k~7FQ zO<=$uAEcGSV*I~O5G3bhg~!tl6VT#1KC}USQuiADGs>Lj3Ue*Mx|}U6nKE}jJ|X(i zS@@4G^S~(*y+r)(pytGT)%^E_S@&bok2s@aer_1V*KijNP8e+g@m}S}f7Ckb6}xh_ z)@yo%y;hF*b4Hv(_c}6gp0NLxNH(``nyrac5KOq> z;S)tlA?Z~A-6!q+qe_1UmP{;O?Aultb^7*Vya-%NlsBO}V_gyM1r~&^_aP(n>l-g; zToI@M&CSVaIiW`UM8{Np`Wx?}(T-%knNqCik>f!tW#mwNyU3bby!y1{Rm2?I&WKO> zI0BMx>%7def+DrCtxAf4OG^$8ZPN|#No>LZ?IbAhlc;}iYkc_E&f`ZvBVnh(a--%R z5g5*G@0!;4+70<`b8EF(4xUQ%A_nnz^6LYx9KQ-EO(tKTA3QtE=_Lh@cvGH$~Pq1!%(#+mM3e0vUy9*pZie8EL zRax)+E>V$YTPiepe~)56Ch#LdL;wv%0KSH|bP{xF#l*dW_qfoHGb4g!a|!Io+D#~c ztYi5$bE&@Unu#04>(LcLsN8FkT5)4n(ShU?^49$-fi?6J=ixb_(jSQT6Ornlgy4vZ z)|!>f#p1E*p>r45SBd=xOhPe$d~vN`?S#y_p{`?0Z0ORkbfH~xr%=?-M0$yy%~z$C zc(nvgY4EecC`l%!P9ZL8=_T}W?aI$3r1p1U$a(;9K^_Vw(AF;ydjlmy!H0Gb5N|uf z`~kf%@hSw|qE=ifgI|}HZ(L12*q;`AaTZ8ovjwiaaXJ$WjrQfOoaj`5vWqrBFHa(} z+YY3trA5ZhWHI_;I^AOX^6jSF?NhY<>?6kuU}uffYL|w?HF0CK1nHt^L3_4#(hGa7 zA^hRC-!iH;`PN9v@liYEUlnuvGtzxCAOX~8c$}m8CayzzAG`lqe)I~EEiCw9#wLDt zCLo|BAJWwydR2uC9OCDoO*{BsVfaW~XaAQQ+p(99ne7JzNrOp(=cWXtsGq5zY9M~| zxf>bcqVe%bn1RPgM^$fhTInyR zkS-s1SM=*5ox;xBzxNE9mENc}W8$^cB(8I%F`n*o=;c)id1DA*8){;}#KNUv!5`2< zG&)VnT$|fsYCN<2N9&B7e6YDLIpwd-ht;CZ8S6EaRHXBkR|4YNL>I2Spv-CT=QMu|@@ zsp~Vff2*rXxY7bZl&Utaxu!bWJk-{2aUnl(BjB3&h#$*fL2p8OVo7T|C&7LV^dhOp zehT(tJ@DeZ;v#g)c`NsqgyBOXORBvEvW1F9$$nC7M{!1jo4`q;vaMnt4ZM!|imj`5AFk^sDX^<%4-f9Lz zAg(_N51SZKtv6{*+Y?|8HjCY<*0zInj(HQ6wLc90(-pZ z3qAI7)?|^+6-9`ViPut7^w9C0n0;56j!wt^JFuq7e)5p$z#)^?QDdG#JHAqX=m{A> zHZOKLW@4eFz(UYiD|ShY*GVBVnn@1w=}~VH2}hg%I!g%14nbLybo}l7!RkktJAw$F zFodPs$Eb`c1b%Y5EWNPF5_-cFOTZM6#~5J{VS=Q~U%UMB9WQ>5UW2Fh zm?C3Gb*Q039{|~}eCe^D z_ZSX6Yl0=~-2%)v<)M!}42}tSs@V;fgHP`6dlz5X=fm?T0}zZRd%T!dXa;VG7S{Eo ztGt9*>;t&7=3K*=AuCAFStQ0+t|4Z{_3iVPvoGMH{V-C()hLR`o(J)Q7}hIE9rXZ> z{y9^f4jQ*ks_M}cE$DRthUB`#W^-Uit%4$uEiJ475=&i%VtG+bz*4&b>O!PXhYm>- zs$@;Bhyciw;=#&EcD!7hNf=W{%5JPCd#- z>2x_*^jZ~hDe#6uztq8j9`R#D<_bY;;9hbQDQ%dsJf+tChWR|QT(RpL`_4gd7c`4_-!_{4bnQekDEczi)kw#&&FNs3y*SH#Dg@iThscs zv3n-x&PihABBn~G0s(E{ocJe*BKChR@A$tBD5s3XP@6xNl}9!pAi|#^iub=5(3=0% zxICG@Cr^SfCF-k(mn1bclRy>~K_*QHmDPmRG)wFvElSF8GXFKy>{d}|S+kDHG)-$=)M+BE#pKf!Mw^sNq>LbKc9Gr+@f-vdHiPvY>mkUsdvsjbK3I zB5=BYasTSQLr1_N=~5K-?0IJ3n>54WQz7^A!U2%_>`p&>zUX7MD`2)6kozwT=H1_yy|Ogk@#hF! zl8Plyl*?rxLx{UZUriK7Y+8X! zn=#-HyNj>yvf?hBxsbuaaG&PSV&^QjBiO~DOAglSo z_J1GLkYj|Dn$QDrJx>^NrfUwn`ma0vTqa ziVwIw(20VQuoa>(iFq7zE`FepgGG4P`_ZnkQSN3L6P4bfzt(4zuP^OHQ-u&AH87dv z&IpKlOwMo1%Iv6Qxr|m1z;`gtV@8*-D@>Y~Pq%^>-NWSoBbckE3iXC`n-_KpWCcx| zt)n)Ea9Ljlv%!N#!(^E&bTJ(fcA)_PrI=dtLr(>nGJ*5h^_d4qA8DrNtwjPr2v?Z6 zK(0O@hQO|m=@Cv5zYe*P1FoaAL6ky>a|}XyqZ1f|hSn+IGi=*WPN1oFh6Gtl%sJxM zN%*BTcb6iS(D>4~K;@L>vjjxoMu z7Y$FDNra-bbtX5YH`!GcMEC<-Fy4Z~Wx6BV*nzgD>(NK2ulqF_nN$}e$0F-DFfUnB z^l`h92bgVr%I)Jsk)h|fT+^uZXU5DWmfAC@)ceUyg2Y*N@L-d2 zXs8Kolzvw0n(RACa_L+Q_*kH{s*=+j9%9qi!Rtj+=@_T-u;9YydIa@L@G+s)n2;kM z?N`|m+G(1|DipSpxs`un;kX0+_63a@!5d+Af>tn92Tu;o3#~956A&fV3`f8N@7mCu z*!qKca!(^*$(8pMn^pj##vlQr!eZbSkXpz|MS9_eSR3nzQIcQs!oSj^nbG2M6w{pf zmp5_2*VhbD-zH%p4rw3QM8Uy-83Aqd6-!QqHp~cw;RoK)^)p&yGhR_<#r?jD9oqb5 ze9cRfTAs~9?rm6zPoUZiez~xX;BG?hsM2LuUE!7sW>XFnR)z)mS%y_viG_w@ES$Pw zPd_ydN6~nqj1-ZqT{CZP9HrZ=UcDi(iowiK zDsA>x8k6v5vtTA5WPw)Nr9reUJckwIJBV6jvsp8f%t}8Mnn8`CKPvvty?oE)a4J`W zZV`5-i~#FV?JZNtVL=C+Z=x6UGpVx6Z2khAPc{y-DURP}4&;$52){TC_6zM>`($_Q zp%wf)7T|D8@bXNV7;BOkB77YO{vGiRtIs0F^uq9=5gZRNVPJo8xMVp^19jIA(f#oF zsuH?cC>PqLz>O@$?__5=74xRz64@{FW6XwxPp`mo%rh}%F`YFnWc2yS@ET!oT6ee= zc*$G%AlTXQ(Tswrou$FFy{H!bPVt3qYUv^5lhWlzgy@pFTR`N$tbxt#) zL}f-ZcQXb=q`xTnPwsvH{?$uIPCuhmLuAAu3NFI8$FQ$aQratkc=KG8e|PZ_+kbOU zS?Jnan^4ejd(n@6C)u{*V3`@8OQ-#FRb4@)RFe8Wk_@h~e3<}JKxp3qYJizgk?=ExG|K-qzQnaDRrqwcA1RuD&yLJYlv0Fd4jQgTl7_u0LOY^Xj=5=k16Aa~O2vrZF0()`?XxAXWG>C_ z%&X+i>nk@LON00TG8GbZ@JrB>a+ufHy}5T>{*%~^p)p~1E98R!`<_7-MhniaWcp9f zg)qPsX9L!*v|xH6zjb{>we%S;3Cib&vnBim;)@3!)-KjPvUZxSN%=~()a{0@fzz0f zTvVm;-KF~1k(YLcP;bF5_J(gwc01rtvUa~c+P{y49o?#rl{-s!t2WlH2+dTN89_rx z&uyc(PFa>q1yJz91FWAmtSG?EN)v2#?KePRoGQ!EfB5*dr~wk4JoE$6Nb;%C zSS|(6rt-?^bm89q#l?e^JTj=+$239!5yS>7J^AYL=xayQ6sS^a2>j3(w2HPGz5gSF ztezmnxTcp?A6q_flYs&StFc4H{IbPnQQHhL{%*A@(0S^2t=i9TkGh%VvbNlUoEm_! zI&S!vi1HWK#xJML5bgGU70~mYTEjK?mU5F=wNA5rHm#4&j&}%R$|2mzGWmjj-Y|xV9!ld4@@ov3ZWeOS`yURE+cWr>g_1(Hf zlBv52-ZT`I5{Yw|S9Xw%IrBudBFrcu*Q7lb8sp-Su2~hgP=2Fx3L8h23a|Xl`T$1q zw7}c()K3(pX~&-F_2*aBWOHE=rYuYLc822;`rqC0%J`M6+|`1I*9{<9e{6fU zj?F}w$Ni*hUL(df9?1Z@Qd%zdzK6xz6wugm=?r#-jt# z(8BIJoZQK!{4D^Zh*cV>Sh6eW>t;8I=dZvtkhb-4h_Yq5in)?^CRnnKExPIU=aQd0 zSxky$xLst8C|oRr1>Eft$JyCFo{1D9!OyZi*l!>W@IbSoTgrqkp{TLa%8cWshm!*u zPt%8>q6aIIQ?}31v#A-^ zbD!9XT{lBaG}CGXTL$!aM>8+_j4lsStlUpT;gXan_vz;d>I+uGz~M zsvq~iPb})hEQT(O@uM_veo2Iilfg&G+~M?h$}0&UAcGqU46eIu9e&}N`@z5Gw9Xg{ zE+oYQ;< zltJ-m_)^yG(79aO!q4$Y@P;uaU!r_h!YlHAA<~Fhd$L#Z)e~z~olW-8CdZOusblW! zt{q-Q$fAAr$VFW}T5+EdjZbR$UFA|PP*JpM4v;H#SbI^xhL@1%s-#5|?n&WRCHN%Mkge*0OeVD=v^Ryu@~#ZpK1lIHHC};ATyonDZudQqC!U1OnQbO~gecEdd=w>+rCUN;2h)a(|9O$}c^PyE zq&H?h3#G6e>Tril{k^eu31*IrQ#E!mK42qYorg-a(-J{XnxJ$t+1y8H8bOd>WHu3k zW+wmS^ujYpU3sHN%_?CztHjO=1&?PbYi$5(#=iIj*cpRTUtt%BkE5+CwFfHy;*oAj zS18xRDqG&@*mpGJgB^=k`p;OAN`(CI50-KTWBsc^esMQ7;LHIyBLmzwLBH|iHz3ZC zg=r5Zu&ZT|m7zi_e+D82J%#KackhZyY{k3N~cSQ zAf3|P-5?zbN~1JLEJ#S#(y??&ONW4I=UA_03+_&hB~cbIwyU zyEAj1`y}`k`{B!qD(hrv&oPddfwGZm9WuR=iN?1M^j1bsCZ3n_4aOSWtISBczk!ES zA}Fu)zMzk#qq9cXxyyQk@N21j;|-l!->{VkiLsS~;gOe+!*toGfSeMVJ)GO~5*LQcpM7o7!5bFJ0$R z&u(Og>cGc`n}p`RnZ97Nm$AeW%5@^T0tVZnf`zPFj?Ap40>*ZA=qmZ&eeHM+fJJAlaExmhci>1T8Mx!pXj~pwzcF z(w?Q8j#(;8%3D~NPngGRKgDbzPlBwOB)PaTeem1fz``!7~ z8G<_x5TZ8IEMV5)CDZ)DD2AMOI3{1Q2^Yp5>iku=Z(s6sXf> zpmlfw*tmV(UF6ymKInRj27YHPtVpKGF`?lVv?0?5-MZ%0yV}#RfSO@+@wD_4&DKHv zO0#6%r~3l=wgZW_MlDaj(oGe)h$W{1WNmed^opU0sl2%y)C?V%NXp_{Y|;l%U?CKN z4|*FZUIGam9xIZBh+OLd4YfqvdqV`v42WpN6sV>YfIX5vqF#B4owX|{uCMJy!C{Je z^c~A_9^uvK#>rQdqC!fE)o4IY8t;qaOb({Xw$Muqx^PMpIdFEHWEK7f`WfkW@|Fsj z<1>_T!OC^TvwPnui$XYw;zZFq*a!tK^GDloh32Wi&mjfn5>{|hr1pB%YO~jk@)S>L zwk_7e`8HW+PGsM{;219PR!esX=bw9*cnirM-E#6 zYxrC0YPRUQMAM?a^0e>RT@dOQwQsA%OiFKVWw&8En#y+gAcb78U96*obco%0WSYOd zr>L=x2M+Hf(Dbq&jIaM8oQxf)eO7k=mQ#z0ug7QY{FFy5 zzQl@^vqz8Z?k?qIZ|OTx_#L`t8{m_IDC?k6tSJ?(JLHstsNa@Rq>#GCwPzED$ti5el+A~5pEV$hq9A6XS^GcN- zsjUzmmEdQyXfiZdII7o~hF|jLi+X-Axs&nJ(OY_=qhAifv$jiy`Hzb(4B7>qNtn zt5_LWF|C2*3$A)GqQf^1ZG?{;xLqkg{RU&U+LcXmf0)mHG|2QSVt%6*2f* zPeh)+LhOdMbY=LvVK3v^)lBoaICGl1Ea%UQl0Rsn5f43L%QIkm0S(8+pdWFAN)zu}G{yP2u%2%|hsyHpxilf|*6(99 zWmt|F3y1G4Pkpt9N|`P-_9klR!I}80vwMrSwR@K<1ELuC1MUG{QJB5XxLuisk9%y# z*5S+6gy5Y2`q{{K<)-tb>B!h9Bbjj7Y#Ml}@w`#GQ3_22h_zc=HZM&v>7~>Z*wt34 ziYTZiyPBUnjw1aeM~x!}RUNs~F`+23 zUG>-jU+6x4h}FT5dy|~2jIIOJ)6U6YH+Pz}St_3X$QQFxHF9xbj8FL#7~QL-71KrD zF^amxIK<`#8;XiXgwTjOG7tT@c-Vwlei*Bo=@sZenH@d8wW`CliIpiTNr}HO*&N-q zv`GDw-{TAE8$_Xb#vo~E%jtpolA!;g2`AfS&Q{9H>UkXx6PbykLT;bbl~KlM@L(32 zk(#Ip<~v>($B4A$9*GUC+KROzw`^AO%`9@XUbK`xDJHBjoNp+977Kebt7NZ*Vvy9+ ze9KQmkzDwtn!j%%Ev!F?Yi`koKIx!%?TINCyebEcNYuh+5-s$O9 zwu02dBw%ZW;-p!7>#(5cxt8{uEE{7Y56^rb{Y9jWmDf}WMOEueenWV<<540G#e@7O zz7L8V3y+;=&aJ%*dhL(+_esJ8$oSh6+EY#LYJ6_{&`7vG(eeZ3>bWLFVir`bZV>k~ zh$IUB@xrM0CpX#$6S<$!CD>1}k*=+d7dMqjehgpFs*m=YU}SNQ1RNP}uQe);^WlUe zrViQ4KM}n*Ad3|#G-nYJ6}XzM*`Dpky@7#m#o`~wp}@rLOK-XnL2l{2gt8|kutP7= z>*(lR?bSHEi7B@;!iZyDSEbgSpe_mWfO@`PyT>kail@LyN?2mcB|!-vnj< zkG5R4Tm+BxEVJS2saXT*rA6+`f=H{obq>0Kyd zFk)<)ODP~N=`1+V(T;yP&{>4$@$IinA`H`*%?L$ZLPN_Jv@*Rr8Yy}`ucls}y_Rrp zqa)ej&&UJ1XGOpDTa+9U`Y06T8>aiAtE$4Te?463Tqr-k?E9zwBRV9(?As4-jl9@w zFd%$r!0xt@`;0(dG6^_mF_O1UXmvQ=s9#(28d#qt=Wo&gE)xd|63EBW9A@$nd$P0l zuDz}D-RPjYM1Y-Qhl=Ctywkyd^I{$1uZGkrw71z#8@`T<;sd;sK|1a0E%??a*!S)U zxnB9QEP0-hoD#XkQ}X@sVIvy+iq8IYZIsm#T^oQsWw%wcct;nf9mxk-MmtwhQp{}f zcSSJbm(2}`;0B6dF9~x(k@u<23t2~f3puRwPrv~U-zrW;Er9zhPxv(49BQ!2lH@s` zx})-RngqpX5^84=W{~p?(2Tj_grfAIN;q*a0b@O4G{z!={D$_5_LFg>*Ce#5yAe4< zJ_ZQO_Cs)DEC5_=x2%^Xpy(3XobsgDT#>0MI5FA)@PRri-jc*xAXW4`DqIQ+T%K<@ zF*^VYNmRU5i3f1$dnyeI3rwE(I594OgIn}SRD5F2RB4hijAS6}k#JLPY)#32H&oQ` z5=Hym9&7*&kC=MN7-D`OgpiJa7ODPo9GtTtjb)J9;lW=ih$`X}WjE2_Ys)=|004$7 z!k@)C6o`rg4j9ddU;^|5`wLeee-?{m;G-r^ib^UySQ)3pyQbKZFF8mvBmU%OE>lw< z2g0WBt3Xl%%ce@5^$ zPCbH8Nzc!mW}fCDbrd0FZSKO8860)k;pMS3X`B20^%%baowuDc>stHN1l-ydQMXo~8#n^AA>;#O!$r_g1sZ zpVG7vv?n2HZ-+>mi^Oifsgl=S7k{dDF7B?T>u#u4swQ6P)8?dZt;o<5TToR5yO7q^nC{gMuUl( z2?|3nYeqzNaw;@WX0@|P!tJ<3Y7MJDtHObdsIrci_`)f~{^eLU1}iOxX$i%>L83Za zg<5WI-Xapt$1y&bb)TgaA4Na1QWjEM-$&ykK_#9SeqtHJ~ zZ>9rHnlCPyp%u@1Ojuq$=4K_YA^_S;$t9r!`+mESWZ~4NYn4(;3!b;VdyA#K>`*W_ zAI&(_z4@3Yi%y}4$y{Hq`?EY(0;l~k5w0ojzD}>=czsPsg$9?L_UP+E%qPjrX2TIZ zc{aR9$I{)iG@89r`Upq0KL$B*beN?TM_Z?ToDmNs!=B;N<3V3? z24mXNdWUgJPBENRN~l`#=J}=ef%5XOpD&uiX1$sut?NFyZJHAt0u?i>^<7SN#|Pl6 zbRdrl?_#?%MZ6w1pYx7|ZB@o8d~~T%#Lkm94jgcNXEOe6#K&Q>I7 z3&9cLGT=){NX4i%&se-eGP!}EicPhC960BB*B#AmJmYj-hDZ!{vnlQ*g7-V+4-slT z&v+ZBsYxxHvm%ILq!!a98~Vp8mo4&L^?P@W5v%u)E#eAz3!d~Tiw7)gSrG}}R~`s_ zP$pl2{@#@a(gf1P)&TKpR`ow}#dJc1?Av?4Eb~cvJ|b+ZDoel#GTuns7yjCT``h)vlEL3L23l23nkcDW79$~C5 z)t{2n&a*6^oFW=0xJ1-6JbJZ*EDN^R>Z07g!#q#3tai$Mgcv=pd_MBZ63>zOWL8yb znPSFb!+e~V$LaEi$9OxaSVl4QUMlGNWr^F?6Ic;3yFJk;ZbNguF>v0#tfK@HSBAG!AC^t8(!5s9H~q!@Np4A0DQrA$tO26|7HCEB?5)cj_ zPV(C$1^ktC@b-jPubdS%pxFX->vjKFY z3_G_KTe5k_1qv*g z5n@A%pdHop8s>+I(=E)b^VRky-D}1&p$5F6CH8AhX^kD4M%IT9rdkZbbuAA&cq@F- zF(G>Pha~nUqXgs1V3@!?^*QUu+ayA^N9nbnwDi1=?*@@}(DFw=xo65ZR0o_;-(#-A zv8|dEwEC=IOZ&hKpVy3=JeT!O`-9{LB6l;=o%gQE?MH#5it?-te%l`DSj_8!%JRhO z3TNzfZZH}G4B&XgA-f*8s2vIBaksqgjj>BW6+%25&UgxEj`2}$HSOM282v#i1x>ol37sR-qfB0t?9k6Ex2vGA-J6YvpU z-T+y125;6hAAr^0dM@4#xw9&4B{QkUMQKf$1%56o0J1KKHAkO3_qF$p+;)CX{tQMG z6S!0Q)M@;7(yJUZYP4-dZ1hRtk9K>sJ@xq+$z@8R6(f^eYCj zV8q1tiiY=WIsKCK$fIH%oMPPYG_xbe7K8Gdc^%pC22B*JfwvEzpg8kcSNke7w65JR zFWeG|OgumI8)aLJV|k`P)DY-rx&~N)p|#8b9r{ryGEw&W$QKRC7tcdrb4&CDdE<4y z)fK(&STbOp*3e+*Jw9a?kti6)Q934mhg}tG?uJAI-eYI#Y~-FLMJG43rSV1HI7-*; zi-S`3zUo#M_tzh)XS$i^zuB~OOcS!*MhjA-Vt^I3CD%9%?tu9SDJou7oy}skjjo@0 zuYkXOq>D{T5;C^Tq5rHDxg74NYjEZ+jH#}K03-k+62xW^LX^j;;g@v|;}Y=q%5hGF zE8)pF?XM3U6U_Ag1`P&pOt{~J6Gxg50{~3$l?K1JJb!Woh$9OrWO17K8hoaU&5<3B z`AKk1`x^`2=lMt6hIr32|8ntW9t(JJ8`3}j%Qpk|A0!T7TS5loS_aw2?B;gX>{qo% zB!H-RfZ&C{wmg3q49LR;0eB6;K1+y8Fjv&f9&)?zhm698b`kIeD`#n4*xQ|^pKb`Ci_B&121KSC#oj9X6)}A z!N2Y0`Oor099P7y8M?|1+`2*p{33K^=*P_-AwpC&;OAE98QDzi+c6#x21#EvNZ`23~(g(-!T0$n)CE*y74O$?&HAHMfc z6@C3bZsFi7`ucytn701~{~@#gdyf81X8+r3p1G;^KW9(;q_{%;R|Q)B<@LG$VV zsObMol>IkP_Ag)8hU|*z7y$qP!4JEir?1~zp8sb@i2nrNHRugC-QPI)KF|Na84hLq zYjg=FX@Rny@G~VL)ivQ~Cyj+zrKA-#?1NSRe@d z6!JBH|1wqq0Ms|s@?ZR;MgX5!K~uCqiljew8qtXUOu`j-5mxe55txn&B07x>$IDF9 z0tqwWLiRs6T!HH+)T!(%4G{rsi0_RG{ktL!0HC>1q0XEul{b>85OzV7|8&5O9ihjR5!?O5a~yDY4Gt!9OA91=enwe%;sk=eEub ec=;3@&P1fHgaluG0s!#CU+MS&z})iBZ~q7WrX+m; delta 37613 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�aus#JUD;nS;yy5u_yvon zFl=|q@4QknbD7D^U@V$o9oL?qtHV>=p5N4hw__`KIg=M$Ze-nNY;mr#lWCold+0N@ z>!`(+y_OnUyXf+7i`XM?O%=RL!YD|ARX&^klDWyp(AY(-9imkku*SY^TGlc{Jj>#B zL(sHSr*}tvz7Ik{1DBWI(z+65K!3Vhy=iI&?^_@Gh_3V2?J!IXjiO)uOeVg5p8Cs` zAE|*7&c$y!FRO#i@|=5i=^58sre+Z&1Wr3VRD2OWKCs529TDEmRp`!QP$o@8y@*O2 z{Ep>hLQHmj31kTm(8S_KY-)2JN=Yvwk&dFhP7&jgxVW>aH|P=XIsFnIn_<=?cTKo% zU++_bo0kf1Aj0?dZ5CAniEU}%#Y%UCnc<~T1u_F-?6ZMNG%afY#4 zhWB@Ibs!jpoQZ%3svo~%IpqMsVl`~fn<9t*i-Ij2d0_iZC3`#zycV#`%6NIbz z;qDcV={I=;3KCo6q8^0t^Lc9s(Cdf)s^bmVs^f{Z!^FAs2N%=S9ln#1T~V(h(AQVL02?SLjZ32o znn|1ELC^kDx+Oh%VMhJiJ8~2%!KmECRCz#&FlLPo5H#m_Bqq%$p*o8X&1Zf*H3x%y z!IXO;N4@atBKK}srZZa_lHD#MDfh#($x0b2#NAZwS8zf&9P z=VK!QVhOn{mr)bY{!>w20_Ekg%%X~tu{HJ7oGu~B#$lR;M`~+p3cepIr^_kReq3C} zS>?IU)gxOs!dhzDH7IK@eU9ck!9Eu5spB|FE#8f#U3G=0ZYs^6zG3a%JW*GB9v7cH z_>(%B^?pVelGBD(Zzj57k2@GnKY!_V$OS>@@ZPHm{y^Z4|-{s9UhY- zJZG8RZf7cl4q0c8#;x`}laeq@8)%BG?1fkG%=sHAe_k{Da!?FGY?(c$NAahUlpN|V zA!E4TWfdW#X@nLZRxaepY07(upeImeEEHQSUVkl9?BtO9=>_b>es7sfeh-ZocBCS>f2HTR|>kD--IadAi&4v>12=f8pSG@f1>MeUW zo1Dvi)6Ir4OzpNh5uK(nSw38|_tGASK2gmH9(fgo=nG7c)?Nte!;MalbRe!!2=hf2 z7qiU@F6{VighNmgjkVCW4W>yPLftHvXjtyR8Zv~jCe#!-_Jz?70OJTwS)#>p}iK(>Ec@r zB)Bm8W4pMlT?rJ&bnA_zFHw{&lm$adO|T8dmf_Opi;N-S<~CtYYov2)-R3j^+?Jk6 z5hScyS9lz}5w>8aF|KWE+`5!=YlcFm`A)+Sx< zB(`e{-=niyBHvlKXNcxtTU2FRQ7(w1{*c$>3UVfx1fvdJ{TWD4fB3FPVbQ)ros0vY zWSyZR!eqst@B?~L?hZ9xY|P~m=751OkTcn! zMjmRZ+A4kWnsbpssGM}fzoudm^Z0`us4aYuOMIz{@Cfe%L7w){<_v zig{dDv~r|YTdG7yByF6muINfSD}SW7MoQwQdv3v1&&3qm0#+>LEvhz8eDE?MFeL?`)Gq-IRl6c;eIwrd zMYJ0`Qmxujg_2pWKXYn@iPkEHEf?18tyw_q1kVm1biQT_)uvF-m+)IF7sW@1hTNIe zIEV>NPTA8d($6`Zjw|NaD>L~Vxk&Kj)>KwGBfW@$zK)?Mn*{91lyq0l5;(~Wrpi!U zYb?u%-Z_unhgF!*dZs;FjI^8cjH78EX#Fd$&0s%coS(pf=3?pSf@GgQ_bl>kfT@NP~=xbV*87O$3$%#RTiY1nQY9ockJfwgN1kIUW-45Tz$Gm1v)M zoHU*qBH>kr9;^rsup_-QA?^~MJ4>4+XAnpBP)n~pO`$HYogNo#GBW9-x2oovY}~fK z`QZpFn@isq^IXf^f$F1SY=s#&JhnY^GP zzG=O;+lCCBoH27!)MM`t6F&}@bD9i3*Em)q=iD0=O-vCk94q^47xVF?6Z(_pvhtguf+!SB)U+G(wH)@}4&|Pfe_1F>Mll>*a-#kRA z16qp)ydg|t)unLaKH~QhZL=wY2@X6$U;ndQ>HY`*tMu!}iWkTbF{zAr;%TwN6Uh1# zX!lVmv#sw-RDAiDxW65^cNyh~*lM)hMF|u?yvO``!@N zfO&TLq&+KtGmo@O$VrX^v?q9Ie~`im6Uc2}G%>>MMl`bfElZ5?@2eq@S?Tr%$p8F~ zOR>d@Q{sr_0WB3o0-9h!v`&H1Bx7R#h{@396D7Qin_4Wnd#|&u3SW>v82S>$$QMTt z|3t9N8O*yZC4-t~U(U_UeOXv|I=tVnw-W+cIBx~UD}!3cx~H(!6y-(Wf66m ztVZE0hojwkl-CtF%wf=0NbP~~oYwn!>l!Bwn6_pzyXk$fe?5x3@e-+=7TIu_JdO*O zuX1N{A+tyf#f!f#ueo>s3RpIH?0m?P2>)taqWwzOWx!;&UCoHEppSKABI~HBJo=4+ z?ZbyG3^>a)KeL^ht!Oz@g~!F8z&9DiTpZIly$O4@L5?nR=nlfrswZFYBN^;XP z)e;!OA2+oO7Fh5oTNTph^h>3O8>?!kr+pHYd>j?ZnNO`^ih*mg>G&ni35WkfPn(|G z9<=`3jY81m^x8;{W-dRpz-VWhXY#IrFw5kc18lO+5cIR3-Ny;4hcC9_@?ZABI0wKH z^02=FtTL?#zihkgozL%&S1&^F4ewYy+pk0o0$E@l^vcfROC^OK(WLbke!PoIBU&U& ztl2E(+8}4?0)$O6)ZlyBo=Ajw^NV9@hmCM2lI#o4oF&mWNP91`3OL?zzqv-_Q0;hO46s& zOHzG-vcx{jHJeH+2bv`_{#At<8yHJ7Q6)Al6A~%8nqh_r-p`Ygql#Ih{0%#x*mdG8 za;;p{aNy%*CA3%wEI7~}2BkS5a$S^pbYlW{JfioTC2!BH&PgZ1AV;jZ5?gqFmZ9%GEY!!&)}kAPN_A8zR>w1%0)m zu{54;xep{^KsWVA3&pbV6|#@-g@*Mc)=_3;ZgrgPj(lN;4&ulA9s#>rx@)=-_j>M_3XRPV0a;$AY~kQ?qC za&lcV{B(!c?Aa;G$oXxSxK?AIx1Wk+$7W-`(^sBeO%`=RFJJ3W)kgdzUr6?c1+Ml2 zkzTjocmv>GzCp!l&qCEMuiCy|Wz$=i^1Fy}!b?*W&033B=}JVwk`4|uUE57VIfzB&1Cs@$vSO@lmEaFsstiu*>fqh;rt zSaSP61S?&uEejv_9!IZ+uqu(|apldUbEqMx);55vr2l1rUOdV09PT9THuhr$Lq{B)Mi^EQZ(cmH;WC;T^<0zZISAht;nY>@N<&r1)b&?eEZ!s5Anu(>VDNJ z)+RhUhziagq{;?;$CzsmW_dbQ(^FR*5((|95N(mx{j@yjxR& zw4SukMWIq#H7dQ7fr}7tb|v7P;KD`o<$XB6<*7dq(D~h8A&WmwW3s{vuRJ|Uf~?$Q zjV>m%;=ut%xKu-yEJ&OvRpx>G5%dJy^CR|i9y?ha$3U{c{KqgEejfY4ECXoU zud7H;j@<{%b=3KU#q(#Y_Dz(TMB5VIlQ@tB`&BIX$m#{eh&&}jx^$EII6~Pc>4xY+ zExqLw{E%kpi$x`_DARwSMxDtZAZdmf!uW(lUn*ImspH7+3W}n%N19x@6*ns*X6$+D3-eAu*^w=`fK? zq@0up#4@K4LrSwKt9!c<^!hE#nB$a3!HecSp1XCxQg0mZp263oH4yfCz4PpMN9eP) zfp}z}QTh-pk-~ik+1n9a@(RK{lxh-^Sbh)0BDke9WKsUyfhlz$qt%y6@;Bc9ve#^( zsn(JIp{s9x5-cQAjM4E^j3gjaF3!mTrKTecXkMg6M>)YINtG8&4X7Y-C)-NW2&hMXKn%lBE`U~8elB(J zB7q~vQiEL|vENkdL`58jDVjYi8SBWQQG6nJeb_V#~biIrs8d?$m@wY18A(1P)F#}LqJ$(qtF6T4( z59Tm4nti~@Yhj7cJ8XP}IimwywYA151-SiOxKYOSuJ~&_dm5FF*d3D}z%#sW$bjSu zW|CsVtQ#%d(oMb1AHlM&I@>2^@pJs2GMlq9EA5aT(7k!YgZc&w37Ku_E9JCj9dKBk z%USB~<_@s&^oHF}vaIDPc#aR2Os@f?SKom*EO;@kX^IoIcQ;_Khy9UM6jw4vkS8k1 z5qPyLz+D?bSzt~awg^8r^D{Qx$jg>i>><8h)M@+1HH~N1s5~fR+$Y$7A}GI0mAM57 zARr3llpA)+6oU&qz^vA}C#pKycQ40m$uh8P5{Z& z{bM_j&TUw%&o+56u%~Q?qy#lbDyrC_IVq#F0ZPwVY?rhMfF&3hIy_cXJDZ!%o^WjlWT0q!58tcr9;~ivN5t zuFJo=jafXgih;JS^o?!n&kYck3nZM2`XkOV4zqtPh4=T}0Sp=i`TS)1rUT`2NY^=x zo^JllZg^HWN0Irf`tgsWig@HL{mySCqaC~WFpF;fH;oK6WGx**)2!x{F5i%oBRc9@ z$BtoEKs;513XgB)j4bz*-ZcKgRjY6%m8bz1Yrgk+3g#6NEm;qdaYQI4c)?pfP9VHd*-SFdudUy9> z067Gm!Jc5JHE%-atHwDoW$^dgve0(4o;Xjmo|qjNgg^Y?H?CyAyOycf3vi~JJXVl* z*2-r;hy;}W5$k><3;a5A@7F`~8$$R-%a?x96QbAPgF)DD7$~*eKn2$+*Pcy@JV6U& zpRJ&WVw)=|1Ggz(ud0AQt*TjGwV!RZ1K_-j!s-+_;MC6E;H@}W!>iUnvS0LPU}lp2 z?JmPWgU4Fzp`5QqU-S0@y;EX)26E+UT2=mQJ+m#N*tR*@wZJ_XYqA*0|F0Y z=Bs*){i~9s5wXWR63kVo-h!VE)%``kU>hiKtL0p?keVZtX7?|oN3ymlAeI78{`m%r zW$$`;_=^VUG%TY=Z5Im@H&)xz^0&99e^`-M1^H=hoj~`ML;^ z>F2^Z8<)eb@)}vJj4sb@UoN=!0A#!GD!3x4DY`KXqC+jq9HtOyBAZcuzEBtUa?r#V zk@>)teo7+q1UFe-LJcjBktP<&OCm+hQ5wA(CrIMf!V&|UFiwpSs70?cEA|B8G$LX~ zerl2Ij;w|@51q!^I??~h(^B2f(^^Nl8Tp;=L%GH&_Ke@buy9SEJfy|H2woxlfM<$taX>(cGc~_R_Yg9c(WvP zK6yflD^s-gxYEwNfxzj48GziP`Srk6Ek;yn6!VwSs3jVd!PP!YTHu2lbV=*doK%v~ zZ_3S$P;7-@msk>D(m_Pp%T~rAEHf6^$mm1Fn;3M|AAy|WPXF}N38NKY;SVFu=6c% z01Dubrt{FfW~yIP%w1c~fnN0CQ76Ha+T1*95aLz?sI9YVA~-BkeBKe|`Yfd~!+v_d zvMWloCs#j;npHc*KH#^Q9Q7uVq-mV|y{IHHf+F+=?Jp2}0S%60vuzBtxY)h|E3+Ed zXo;~agT;T9gr149C^gS7yHOPL@Wvy53X-lh7Xq9kzpI=U6!v;&`dCDyuZiHlI7bGX ziC+c(^Y$Yf=_@Mzfn5(jHz41CXNRS{v+w}y6_=IJ#=kvrch=CcX(AKk#5O^j;FE|e zpywj6D6@^21~oC)X0op_9C?dw;~XEBUqn#CMlb__1XFLf+h%}eA*PeBbGKXrP8+ZC z`}+exGe?u-!CZKfCn%+9sN8iVVK`SeW0RwuQx7xE7jo(6UBvL(vBC{s?qnJCRqFsg z+pO5JXJ`r)0~q`0HuNA6p64!<=TFW7SQR&3LmK>H_26Un1p)=|>Bh~YGj#*3KG<9a-Rr?NbIr4JrxO27*4wqDXys++fbyBYmPj*pNGdnAdinu!Fzp#|M%5LQw^SVRh^Nd&k3Eklh6_j$h}OD^ zx`f{U223H-#%~~|@i6T{?zr6GpBM$Ukn+e8MDeWtnynpW9{*QXANBl1{jQ7rs-%*^ zGJNE$hxMRdPR=9{&23 z9%=kW_GzBU?+uDg;nN;se1SC#jg!}~{RzJGY3;aK2BSLl%S+d-AHBAWnlD_A(bDYf zDy`%hhhN$Ths-*%0(xq{Y7;sme4>1}Jp|;7m?xcE*t|G^ zXDJ=bNKIF~#~mBCFp5-l4BHKe^tZid>q2bX8C?902Y>23c)E}tnrn%c&oW^21zFUM zMJ0D5@*V*7JUMO-GR)z?G(s&+NiA|`9vkt;*Em=jybSL!qA;J=MXs%I(jy*Q4_+DF z5K%ex@bCB4avJY@FxdOWVeA0YK&h~52ZjhivN@a82EoQ$p;r*;R;2oDVw4QaSXd z$V;f@cVvGHQTIAgql=>#5`l};IO2|{=yK2lMtZqWCMp-0Xgt@|d}1pnO!dOzp}|vV z(Bx>TDk4oagp~)~!$bGka<=Qz=(fxz((#lTSHSDpXMz_SU=%x@W`AE0&Y*~{_*!>% zW)Ykr=)D&to|#X#_3*+W-VgpWdcB1aHCwGzZjwIVuwt(+^BSv$zk^G~3*n^E(^`50 zS#m#3Az=3DY#tJfwz_X|jM{{B*GYGT4bZvvW}WG)%73-lB$M~}!woC- zJw*4Pgh90b4QryL>+WOw$|ar=*QeMR*aRoS&txxdXMw>~>NyFgFCKh~Ry+WlyFedBBkXx=UL zaH+FnVn{BWFO?u%LV`H<1FcNW^|}j1E+obQ8pdvt358nDUZV6EbbCT4_IJVDSNzgm z(2j1VQ{xCGn>LR0vyNWG)|m&r%3Fa#J_Uz+pNTP(Ra47<37&`$?V}nUyu;^P6#YfT zaDXz3%|Lp1e_2o;M{|*nOi?9ZqqE5=wny9xX|AfnB9b+}OWGOnQs!7ixaCO8u0Ao-!&?_Q&jJp_5>qRoPb7S0JZ6{M9L*utzb=d=FNdplo?Np{QAAfEFQ^K%Sz@_ ztRJ)SzbLOv@;@^v*IYi}-(wYqan1JJAt3)_KXPOtnwj#&>jnzrkJs$Z=LP&fQmBf? zp2e$EKKP{o$5YO!I$&bT$T2OF)5;_HL^!r`u}~CL5Z}6W87^MEw$}*72?wYzjAS5D z6as-aCC(nYNOmbADRb8|bAI=;GuNMQ_n(x3+a%o#)W`@Hr0b>Y5n&;)B(P19Q+r0smy>ou(aF1OF$Afk#3kvjwBvqgWqZ&8NT z`gZDfRJ1IWXoXjev9vt=EzQgO&O;qv%1$HYf@XW&3A#iau~)sOX@q;{KC7f|vm=sFPZY*ffzPv11zn57#|7*E@f>$% zGP=fCs;0)QKn~yZC7H- zb$o)d`)MEC-HVM*{`4Er_NSe+;Y{1%Or#=kdO~AD7$G8jY)pZ0%#3MpbVf9-Y$3xp5#3DJ-b`< z`}uw&50GfMJ(x;FFkr%a|Malh%;a%WxOb=g+Dbz~;YgxhyFTxaF6J&wAENc(N*7L) zeXI3XTRh;2?z?ltm5NpG9%pD1y^sEot1=wYV<2&p8^M0QHlpu}gLZeZ@t^qFE1blt zJAMB_BvAuYyT<)T+2+G&pJ^8SI0v*Z*|z`YW!68?>3My_%9#T8qR8yIBio1rvCMd< z91BS8>p8S4x3QS2G_rSwf#WBVC?q_u04c7gJ4t}hrW!w5oTlQ4Gv8I(7_V=H1}UAV zC5$?J406Y5LupfLZoh%i#3yWaVZQ^XK5|8kyh1Tah ztQ(+IOOh@;>F2M?t3;9%I?|lX zULb%Z!Z=c`bk-5f(h)8`kb_YuUCP}^grI04UxPYdL{VQ*SzlO{-OMLWZN;YQS@8un zN>3ht$~85L%{%A{<|RlJNnwgTO_5mK_&J~%_}2iDBFQwn+`Yh2XFLkw0E(h`XgaUF zsmM}y*cS36{Wfs|YA1Q@KEvt^NrdlFg@^AD@^p?1XV`@*WWmcHIv! z`}lyB=#mgj8uI_t1{4zqx=DXxy8OVn9|oBwQCBl!Szu%DLg_#q)#{vQd+euWH3Q{8i1QbPMuphkmr4cuDc zMn~qv!d>mfj-^16mz%H%-W=X#SZH84UfcGN_@*@UTO5J>2KuHt?4-}xArj7RIhE|c z&%4Rp`ue)R#|#8*u{Tq0P7lo?y!!2W!TS)f=*zPr2TI zHTzSAXz~Z(TDAlmJ|u`SwS3EFEm3YDMb(2z$H|@cwWNKM6 zGU1t5c*DJ8#DeTn!b|4XfHHxW6Gl;(`=aTVKV!{OesutFw&>6nB~FY8>Op$<+2xbHp-!Zp zz~ydgklnDRJMmsyl(hE?kg51a%r<%sabt^$`(VG0O*TH*GBeW3tEzwZf&`*@s$T=)t;y&N zJ!QXmZlur|^gP5P3_&f@g>@e;y1F} z2mPDQ+}3ccDgcBn%aPugggB$+lPp0nADM;R#wF4*gdC3)Z-fdGWE24w+-U|Iv?<>) zgfelBvN79aycxnD2v2MWG(H~ixtp^%bA}!Dq2#IPsqU`szPi4uSdZ`fgcPWKr$)eY z+i3Fz&}8plZ^X#AdI>>b6j;2a{;F_&V}<`N-&cHnffraWJcPyfeo+}V#wWc!^}`!n z^t&8qUd3AvMbnIVA(tG-np_^O z(jU84qBQR2HQKv6+a0G3;F^s6R2N=7l}Ce@FRF=gXV z1x1Scic}*-`9&tJDqB|7Bpd#{c~`s+x4iP)LW$V8iI1?1`$}aR4)t=0-na|-Kl`ks z3>Y7AnpK%J!>UyVR(01dhUOzU_UzipT)&!hoc`R(Nj>)5^0u1CX}{8EkbXL~eoX5o zfR-dH_%sUMvDAeZ*i?-LcKxJEKb@)v{d%LiM#Z9b0JpC9DW3j?c`3u`6iq*y66&uA zwu>g6wU=YQ)((vniXdgesLV$5eIq`HG4MkTPjZn(DOD1T=oZG9>ob_K;3zA(6!+lC z>0sw(0TfQsTO|^=s4RIhs)o+x*Evi%0GD7BKgtevBj!4osWa{yt71c& zFw<}IHxOZEQ7VGdwI>_>njM`YdUtzhR*U&Dc595-R*N~9(SxY6Yx@;jNv0(kVA(WZ`QVS(sM&3%9o=B&>h+px!<~-S#yaECHFo#EYqeTi04k`O z3pfLmnrns7p;hMQoo)4Aip&Cyq}O0&W!=qEerfFDr3&0&{gO#rm{!T)sB0{Gy`;{r zfiG+XiQm=s!1tQsrdO+1jYhT`H4e}bRrp&PVI*a>mmlJ(+&owalAau)9$B^V-C$F1 z%xivQ-p=MTZ0PvRf7kDn_&VtA083GQ;Du6DjZ%raDpRfxbTMt&GJxxqjb76nY~6Aw z@UZG4`oVv1Msy_syeYrLBqNi{{ldE1nk#GCW#hms)9rBV9Ie~HVrtpt5y+3);XX>Z z8B8LjX+2sqCl@wJGIf(B)aUP2q7fZ=hgCLiYmA-}B@}ScPE!w9No!;60R(dVAw=1+ zY{yENkK|+vc16pRxb9DL@HEh-J95ToZsxQ%E+h^R7Z}AU?&_jB8P*#eGl6I4G7w=o zdD9H0*xfHCz;jAo{7;um(Y?q5>)zNS|sa%2tcBkGD zONu3R)iJ>Kl*s!-krqOy8&)-|vE;5}ztpGPM`t+xLM5|;TTj)(jHdNYzaph4T)Eu!{10*b{|&jw@Pl<2vR4aahVm01F#j|m^P zh(QN(V7>xTF(~TexpD*3?X67_LEF`ixn0RJyBTDS%BfTtZm;dGmB*h86~?7+)mZVc zzX+c134#P;pi=#%4Y+hj(!ncJHsmc;&8mdd)tT|~`<v6X3L|KOsahndQomJ&nVU}++@nS1}K;p&TQ)Yfdf?GMo(hhL_45O5STUgsI1}YIc@Z#X9Fh z=uKYOiPK@kVFO&^RD3@Ev_OUFs*gckb!pVG9_hOD5_3{XZc}Muxup1QCFUI4h&l{j zg>TceW8dME)4Xp(j5ZOsPkU->bqO}x$DocdDjImM`&Zf^HHYS-YCuHg>RF4 zQpUM8ur}@3xq~EVbIa{-CSZy7Q9hexs$j=OlIwRjDf_yM+TyO zjL%js`9Yk=km1|;qrX6GMG2JlPqGN|2__?XaDm}pe_r2Bgxn!LXF1?6CSZx*9+-?8 zO$^hFj&+*mQTU>N3TS{?lOr|O@MBp(Z}x_GnpE|eTbt$>@R|DfNjOs(;K7a07}+X1 z)871!+yv;9w!AdD+>?fXh?R<5H5rki$fH5(>c$jX_XyufB(u_U$nzn`l1F=nqu`IS z(paitzhKm@AC-sm(Ii;kv8Ev?50)9r(8Ws(@sH9kN|9hiD=pT=8-<>Xh|rJHrsJN~Anr zAvTR7e@-Tj3ilJ3$5k1LGFTdp@_hDeGg?~*>721hqM z^=83qnD=|g$j{s*e}sVy9P}yDIu#%(Rxo3})JU;N)JEcDX?FTqu>x5!yZR4y^m6bQ z%K)6C>y`#bTjptsC%Fqt9>m2v^2aitT0m?rB+Mz-?M6pf3pQ7v zI=*+tJ|UkM;m_a+zLjv*9D1u8^qEF&42Y$tcBj>W^#JqzKV@BcJd|A|s0`&Uj2iPFj%>z){#u-x%fp`3$Pog~sYKBM=6ZO$VrD4BX#&c8s zN{jY0PY{JUp9riy*#+XaH7;EJh$E>f(O;98(^ve*7NK1(P?L_ZlaRb;RVdr1vvQqJ zmX8>&1*Gltt2Nq6kEcBDf8wQ_xo2_Gzo%vzQ-rHDjqVanUU3#y$Y=M1AIr>&e}P;( z6ZlEMe#>EpRC+7i@sdyVp0$GU#lxwioKF_qzuF&mksovEr}b#w9Cc8k8If%CtQo`K z&wkJKm<~EQrW?VS$mKqId;K!0)CVdQ^EzN2=3rv7yU5L#Y3Z#S{ZW9mfHKpSQ|O=z4fV37CI z4=4HvemYpgy0mIuWLARDITPStS@Za+qjl9lXnq+n->)Pu13rHD#WU{lMA+Pe1|Jvh zbh>gmJ6T;`>niOrz*{1}2fnm6#>Qf-i}63pu6C*FT3#L;u)il`eOTxrEStR@vPCRy z98-?fu>tHJ*1Kh?&yf^T**KRrFSr58b=}H>ylYYy7S()J(^dp^y|HM%tTmxRx(b!n z#+0%KAtMzV4Q)j{u`%Z(TB|JupH;xk&vxrbshL3u$*Y<6dXAFkdVEk0= zL@!mJyZJ7w;~H=|Cf(R@`=7~9;hhV2VZE~nymvHGaV$n3JsfcJiaTQ}=rqwsIXQ(D z(5|3P?ZY!Y(QUT|-EZBn7Sqk`b5sw0n0`#ZdR%0&126y14K(9@RoDIO(u0j{T<)EibVO<#+J7ihwEe~P8$nhs-%|@Ie_Z8 zmwX_ntIOj!;dSnM+gV~Q)Jb)qOdJ7T_e3g(>GwQ6)5ho>`>xN8!#&Y|9|?2FEyPm7 z_kR&eH;db@uDOmV*<5^hS!jmG<=*ttrd@w}UyD5V?1a($c;l-Ap#&e~LPKPNGwTFO z$v;_KHof`VhLChyy=9YINrrDj$M{ntKYvTX-l+7^n@ry#%_0IN>I-ecRQ*{ZBA(;2 z1FvgjbeN+RT5Y#j(kGFDk+nq_bP0C$Ktyt#0W-2~H0%^ZcIyhGOUt@VN_+Ojoi|c! zok+6`^4Y{&N6FnZGcy-*qgl6?& zqvDE(R=T+8`zo#Ps{`%J!@iANS~ZV&mfyLTCoI=ZJmU{LaxP1UH-UMSet^I9gOg%S-c}a3 zYhnx3jLW$s&O@m(VX5>J4TfFDFwL(N8af^7i%wgN>-G{p?%vI^g$x4ERfNX9H;)zg zZwS?2Ii!_wC62A&=_$iQEK=9BO#b;0U0@Q9ywwZayy(qU72`L;^mNmSJz$}B0!w^e z-9q@XcPqe#yH0*EK)72ZUko+!sC?hmQ7v5$h$n41bZSxMS?wzKsL z98$Twb|1bU5F>jZ-#@NdgoqqzmBW7uxyWeJ+<0L+hTw5Pyh|IwUzo>j z|2SD~(&oWTF@SK9b)CTYHVpAJ^gAL}@wF;`2`S4ffcWH7Lo$dBjA9Xscl9cYZjr&;3GK@;E$f z7%I*r<{};Go#Y>nekYyU--29n=B_=Z8D3@_93OK32}6&Si4$g38{csLJpd=kdL$Vg zib^MmEKga)4Tj2IQSi(lt#|-NQ^f^mh>6)vxduDJ*-p|f*8ai&pJE^ZzwB)Aq*;%0}v7P~G z-Db&;mUMp*K)_5~_c`Lq_V4U0Yk>jIr(u}jYpwbOae|XmDYV%Xo9jf(Dp0wlm76F& zl$m(5Jbhlxgud2pC6M6`+)@9QuIDFpOR{wT*dLvku zw$Dg^BH_|p@yVgN7(J}-EeBD2<5^|LF10;SGtSTx^kT=G)_UgGj+qNP7Oy?`i9FpE zea$_VhB4Hz z0C(Z}@5;(w4%k1_>j+dGbIG|Lt}4-@ViSQ;oA~<3(_8KigJp;Lkg!QkMBcLIkZ{n2 zl4hPM!_rur%;n>rLAHcXC880ij2nKsDq)$XRo-msh8QVHzI65t(&7em>PF6uTxQ); zxi1~BORAq1DfYMAntE@wqqh}*&{cQ6wkGLo<#qQs^ZC}sZ^_-Pb|J-(Jq(K8`AKlB z4YyLMp7~AljeqVc(Yr2P%kn@WROa zVyQ^cnQ121 z8gq2UkX6WpMBM)2zXzBblbZh3c( ztU*AR9|T}P@`?ZmG=RV*G$4^I_SbAsRq3J{10eq@z96*2rO9pbLU?A{{9RYwT(ji{{WN;Y# zQRvrfl8}^3P!$XkP=MJq6UDaK4Ykt-PeLV_AVHDpv0jCc>D)8)K-n}Wg=9B;=sD={ z5C9L4B$Y%j{HNpy1)Utd0giCz;E&rvzZ9w1Q&D8P-wY3h|C)TK8_1i1K_16|&6)so zmhve6xp_-Nqx`EY@HG{CHlEy@$t*8LZ{({=K;$e8;)WwPH_h@;*tUE1BMW?#I&kk( z_vV^HrsL-J0!SHlKyQxHh+Xe|87RvFWwKO8c%=T6At^@Lb=RkZABQi%S&*Zm2dDoY z<-k0g!tNhle6e{LBp~xQrG0)c=Z``z|2@NGw(y^~8=#)lKxv8j(aLOf1q!-{6 zHt%{VhQLR(2NzkDie8sbrrVt1`p>LTxh<$9J4K*`7xZB4{k=7rKcgU52mp_<;BUwR zgseY+5HE1v>dz*|gK$VEF#7ovxY?n02tav}0dlCKMh`B)?kBhujTY!H5%1ak F@Bb2_FB|{> diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 78e8b07ec0..19d44294b5 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,9 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.1-bin.zip +distributionSha256Sum=03ec176d388f2aa99defcadc3ac6adf8dd2bce5145a129659537c0874dea5ad1 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionSha256Sum=9afb3ca688fc12c761a0e9e4321e4d24e977a4a8916c8a768b1fe05ddb4d6b66 + diff --git a/gradlew b/gradlew index 1b6c787337..fcb6fca147 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +80,10 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +130,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,6 +197,10 @@ if "$cygwin" || "$msys" ; then done fi + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + # Collect all arguments for the java command; # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # shell script including quotes and variable substitutions, so put them in @@ -205,6 +213,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index ac1b06f938..6689b85bee 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java index 1a0e1147f3..ebd5414c97 100644 --- a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java +++ b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java @@ -186,7 +186,7 @@ public static BrokerRunningSupport isBrokerAndManagementRunning() { * @return a new rule that assumes an existing broker with the management plugin with * the provided queues declared (and emptied if needed).. */ - public static BrokerRunningSupport isBrokerAndManagementRunningWithEmptyQueues(String...queues) { + public static BrokerRunningSupport isBrokerAndManagementRunningWithEmptyQueues(String... queues) { return new BrokerRunningSupport(true, true, queues); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessorTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessorTests.java index e58b7cbaa9..29b6518906 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessorTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -388,7 +388,7 @@ public void handleIt(String body) { @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @Repeatable(FooListeners.class) - static @interface FooListener { + @interface FooListener { @AliasFor(annotation = RabbitListener.class, attribute = "queues") String[] value() default {}; @@ -397,7 +397,7 @@ public void handleIt(String body) { @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) - static @interface FooListeners { + @interface FooListeners { FooListener[] value(); From d8f112307fffe3affb5c7f4f7a24448ca4f8813a Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 27 Jul 2023 13:10:24 -0400 Subject: [PATCH 268/737] Fix Micrometer Docs Generation Do not reference the full project dir. --- build.gradle | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c8d25d450f..9671d8e6f5 100644 --- a/build.gradle +++ b/build.gradle @@ -552,10 +552,20 @@ task prepareAsciidocBuild(type: Sync) { into "$buildDir/asciidoc" } -def observationInputDir = project.rootDir.absolutePath +def observationInputDir = file("$buildDir/docs/microsources").absolutePath def generatedDocsDir = file("$buildDir/docs/generated").absolutePath +task copyObservation(type: Copy) { + dependsOn prepareAsciidocBuild + from file('spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer').absolutePath + from file('spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer').absolutePath + include '*.java' + exclude 'package-info.java' + into observationInputDir +} + task generateObservabilityDocs(type: JavaExec) { + dependsOn copyObservation mainClass = 'io.micrometer.docs.DocsGeneratorCommand' inputs.dir(observationInputDir) outputs.dir(generatedDocsDir) From 2babe75dad7d8ec0cb9c7cea01344b8791b4348e Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 27 Jul 2023 14:43:45 -0400 Subject: [PATCH 269/737] Upgrade Artifactory Plugin --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 9671d8e6f5..0e24f25aa7 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ plugins { id 'org.ajoberstar.grgit' version '4.0.1' id 'io.spring.nohttp' version '0.0.11' id 'io.spring.dependency-management' version '1.0.9.RELEASE' apply false - id 'com.jfrog.artifactory' version '4.13.0' apply false + id 'com.jfrog.artifactory' version '4.33.1' apply false id 'org.asciidoctor.jvm.pdf' version '3.3.2' id 'org.asciidoctor.jvm.gems' version '3.3.2' id 'org.asciidoctor.jvm.convert' version '3.3.2' From 0817b9a3a9a70149f883b5e98094b9806def0fb7 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 27 Jul 2023 15:49:50 -0400 Subject: [PATCH 270/737] Fix typo in gradle.properties; enable build cache --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 60fcf260b1..f44d96f2ea 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ version=3.0.8-SNAPSHOT -org.gradlee.caching=true +org.gradle.caching=true org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.parallel=true From b8606062c04c9f5b38a2be93db03b7bbd8113bce Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 2 Aug 2023 13:33:45 -0400 Subject: [PATCH 271/737] GH-2504: Add SmartLifecycle to Conn. Factories Resolves https://github.com/spring-projects/spring-amqp/issues/2504 --- .../connection/CachingConnectionFactory.java | 26 ++++++++++- .../LocalizedQueueConnectionFactory.java | 30 ++++++++++++- .../PooledChannelConnectionFactory.java | 45 +++++++++++++++---- .../ThreadChannelConnectionFactory.java | 29 +++++++++++- ...hingConnectionFactoryIntegrationTests.java | 4 +- .../BrokerDeclaredQueueNameTests.java | 4 +- 6 files changed, 121 insertions(+), 17 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java index 1502fca5b6..b7f1c5b240 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java @@ -50,6 +50,7 @@ import org.springframework.amqp.AmqpTimeoutException; import org.springframework.amqp.rabbit.support.ActiveObjectCounter; import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.SmartLifecycle; import org.springframework.jmx.export.annotation.ManagedAttribute; import org.springframework.jmx.export.annotation.ManagedResource; import org.springframework.lang.Nullable; @@ -99,7 +100,7 @@ */ @ManagedResource public class CachingConnectionFactory extends AbstractConnectionFactory - implements InitializingBean, ShutdownListener { + implements InitializingBean, ShutdownListener, SmartLifecycle { private static final String UNUSED = "unused"; @@ -190,6 +191,8 @@ public enum ConfirmType { private final ActiveObjectCounter inFlightAsyncCloses = new ActiveObjectCounter<>(); + private final AtomicBoolean running = new AtomicBoolean(); + private long channelCheckoutTimeout = 0; private CacheMode cacheMode = CacheMode.CHANNEL; @@ -446,6 +449,11 @@ public void setPublisherChannelFactory(PublisherCallbackChannelFactory publisher this.publisherChannelFactory = publisherChannelFactory; } + @Override + public int getPhase() { + return Integer.MIN_VALUE; + } + @Override public void afterPropertiesSet() { this.initialized = true; @@ -459,6 +467,22 @@ public void afterPropertiesSet() { } } + @Override + public void start() { + this.running.set(true); + } + + @Override + public void stop() { + this.running.set(false); + resetConnection(); + } + + @Override + public boolean isRunning() { + return this.running.get(); + } + private void initCacheWaterMarks() { this.channelHighWaterMarks.put(ObjectUtils.getIdentityHexString(this.cachedChannelsNonTransactional), new AtomicInteger()); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java index eee82f567e..e83020bec0 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2022 the original author or authors. + * Copyright 2015-2023 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. @@ -20,6 +20,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -29,6 +30,7 @@ import org.springframework.amqp.AmqpException; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.SmartLifecycle; import org.springframework.core.io.Resource; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -49,7 +51,8 @@ * @author Gary Russell * @since 1.2 */ -public class LocalizedQueueConnectionFactory implements ConnectionFactory, RoutingConnectionFactory, DisposableBean { +public class LocalizedQueueConnectionFactory implements ConnectionFactory, RoutingConnectionFactory, DisposableBean, + SmartLifecycle { private final Log logger = LogFactory.getLog(getClass()); @@ -79,6 +82,8 @@ public class LocalizedQueueConnectionFactory implements ConnectionFactory, Routi private final String trustStorePassPhrase; + private final AtomicBoolean running = new AtomicBoolean(); + private NodeLocator nodeLocator; /** @@ -233,6 +238,27 @@ public String getUsername() { return this.username; } + @Override + public int getPhase() { + return Integer.MIN_VALUE; + } + + @Override + public void start() { + this.running.set(true); + } + + @Override + public void stop() { + this.running.set(false); + resetConnection(); + } + + @Override + public boolean isRunning() { + return this.running.get(); + } + @Override public void addConnectionListener(ConnectionListener listener) { this.defaultConnectionFactory.addConnectionListener(listener); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java index 186e0ca316..a1092f1f7b 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-2023 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. @@ -36,6 +36,7 @@ import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.support.NameMatchMethodPointcutAdvisor; +import org.springframework.context.SmartLifecycle; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -54,7 +55,10 @@ * @since 2.3 * */ -public class PooledChannelConnectionFactory extends AbstractConnectionFactory implements ShutdownListener { +public class PooledChannelConnectionFactory extends AbstractConnectionFactory + implements ShutdownListener, SmartLifecycle { + + private final AtomicBoolean running = new AtomicBoolean(); private volatile ConnectionWrapper connection; @@ -132,6 +136,27 @@ public void addConnectionListener(ConnectionListener listener) { } } + @Override + public int getPhase() { + return Integer.MIN_VALUE; + } + + @Override + public void start() { + this.running.set(true); + } + + @Override + public void stop() { + this.running.set(false); + resetConnection(); + } + + @Override + public boolean isRunning() { + return this.running.get(); + } + @Override public synchronized Connection createConnection() throws AmqpException { if (this.connection == null || !this.connection.isOpen()) { @@ -180,16 +205,20 @@ private static final class ConnectionWrapper extends SimpleConnection { BiConsumer, Boolean> configurer, ChannelListener channelListener) { super(delegate, closeTimeout); - GenericObjectPool pool = new GenericObjectPool<>(new ChannelFactory()); - configurer.accept(pool, false); - this.channels = pool; - pool = new GenericObjectPool<>(new TxChannelFactory()); - configurer.accept(pool, true); - this.txChannels = pool; + this.channels = createPool(new ChannelFactory(), configurer, false); + this.txChannels = createPool(new TxChannelFactory(), configurer, true); this.simplePublisherConfirms = simplePublisherConfirms; this.channelListener = channelListener; } + private GenericObjectPool createPool(ChannelFactory channelFactory, + BiConsumer, Boolean> configurer, boolean tx) { + + GenericObjectPool pool = new GenericObjectPool<>(channelFactory); + configurer.accept(pool, tx); + return pool; + } + @Override public Channel createChannel(boolean transactional) { try { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java index 652164f462..d31d4413e4 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-2023 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. @@ -31,6 +31,7 @@ import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.support.NameMatchMethodPointcutAdvisor; +import org.springframework.context.SmartLifecycle; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -48,12 +49,15 @@ * @since 2.3 * */ -public class ThreadChannelConnectionFactory extends AbstractConnectionFactory implements ShutdownListener { +public class ThreadChannelConnectionFactory extends AbstractConnectionFactory + implements ShutdownListener, SmartLifecycle { private final Map contextSwitches = new ConcurrentHashMap<>(); private final Map switchesInProgress = new ConcurrentHashMap<>(); + private final AtomicBoolean running = new AtomicBoolean(); + private volatile ConnectionWrapper connection; private boolean simplePublisherConfirms; @@ -106,6 +110,27 @@ public void setSimplePublisherConfirms(boolean simplePublisherConfirms) { } } + @Override + public int getPhase() { + return Integer.MIN_VALUE; + } + + @Override + public void start() { + this.running.set(true); + } + + @Override + public void stop() { + this.running.set(false); + resetConnection(); + } + + @Override + public boolean isRunning() { + return this.running.get(); + } + @Override public void addConnectionListener(ConnectionListener listener) { super.addConnectionListener(listener); // handles publishing sub-factory diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryIntegrationTests.java index 14cc4663b3..668237258f 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -291,7 +291,7 @@ public void testSendAndReceiveFromVolatileQueueAfterImplicitRemoval() throws Exc template.convertAndSend(queue.getName(), "message"); // Force a physical close of the channel - this.connectionFactory.resetConnection(); + this.connectionFactory.stop(); // The queue was removed when the channel was closed assertThatThrownBy(() -> template.receiveAndConvert(queue.getName())) diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BrokerDeclaredQueueNameTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BrokerDeclaredQueueNameTests.java index 96b67ee418..d65698a333 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BrokerDeclaredQueueNameTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BrokerDeclaredQueueNameTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2023 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. @@ -124,7 +124,7 @@ private void testBrokerNamedQueue(AbstractMessageListenerContainer container, assertThat(this.message.get().getBody()).isEqualTo("foo".getBytes()); final CountDownLatch newConnectionLatch = new CountDownLatch(2); this.cf.addConnectionListener(c -> newConnectionLatch.countDown()); - this.cf.resetConnection(); + this.cf.stop(); assertThat(newConnectionLatch.await(10, TimeUnit.SECONDS)).isTrue(); String secondActualName = queue.getActualName(); assertThat(secondActualName).isNotEqualTo(firstActualName); From 5dd01b730bbfcfb94a93eb5ca71d103d963a0efb Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Fri, 4 Aug 2023 10:18:56 -0400 Subject: [PATCH 272/737] GH-2504: Clear Deffered Channel Close Executor Addendum to #2504 - when the connection is reset, the `channelsExecutor` is shut down. It needs to be reset so that a new one is created, if necessary. Also fix synchronization. **cherry-pick to 2.4.x** --- .../connection/CachingConnectionFactory.java | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java index b7f1c5b240..dcf0dc3ce3 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java @@ -846,18 +846,24 @@ public final void destroy() { resetConnection(); if (getContextStopped()) { this.stopped = true; - if (this.channelsExecutor != null) { - try { - if (!this.inFlightAsyncCloses.await(CHANNEL_EXEC_SHUTDOWN_TIMEOUT, TimeUnit.SECONDS)) { - this.logger.warn("Async closes are still in-flight: " + this.inFlightAsyncCloses.getCount()); + synchronized (this.connectionMonitor) { + if (this.channelsExecutor != null) { + try { + if (!this.inFlightAsyncCloses.await(CHANNEL_EXEC_SHUTDOWN_TIMEOUT, TimeUnit.SECONDS)) { + this.logger + .warn("Async closes are still in-flight: " + this.inFlightAsyncCloses.getCount()); + } + this.channelsExecutor.shutdown(); + if (!this.channelsExecutor.awaitTermination(CHANNEL_EXEC_SHUTDOWN_TIMEOUT, TimeUnit.SECONDS)) { + this.logger.warn("Channel executor failed to shut down"); + } } - this.channelsExecutor.shutdown(); - if (!this.channelsExecutor.awaitTermination(CHANNEL_EXEC_SHUTDOWN_TIMEOUT, TimeUnit.SECONDS)) { - this.logger.warn("Channel executor failed to shut down"); + catch (@SuppressWarnings(UNUSED) InterruptedException e) { + Thread.currentThread().interrupt(); + } + finally { + this.channelsExecutor = null; } - } - catch (@SuppressWarnings(UNUSED) InterruptedException e) { - Thread.currentThread().interrupt(); } } } From 52d8ce5e0c9dd3546989eb44e80363a4e075e19c Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 9 Aug 2023 11:38:19 -0400 Subject: [PATCH 273/737] GH-2511: Fix Container Stop Regression Resolves https://github.com/spring-projects/spring-amqp/issues/2511 When inactive containers are present, the `DefaultLifecycleProcessor` hangs for (default) 30 seconds waiting for containers to stop. When `stop(Runnable)` is called, the callback was not run if the container was not running; this was a regression caused by 1fdfe650ac1b6b28f11543af026c94b5128f1ba5. **cherry-pick to 2.4.x** --- .../amqp/rabbit/listener/AbstractMessageListenerContainer.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index 9a938ae690..7392fdab3a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -1293,6 +1293,9 @@ public void shutdown(@Nullable Runnable callback) { if (!isActive()) { logger.debug("Shutdown ignored - container is not active already"); this.lifecycleMonitor.notifyAll(); + if (callback != null) { + callback.run(); + } return; } this.active = false; From f6b1735e8845c4119993459f6fbea04d7d997dd6 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 21 Aug 2023 14:52:14 -0700 Subject: [PATCH 274/737] Upgrade Micrometer, Reactor Versions --- build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 0e24f25aa7..b5a5c9f1b8 100644 --- a/build.gradle +++ b/build.gradle @@ -61,12 +61,12 @@ ext { logbackVersion = '1.4.4' lz4Version = '1.8.0' micrometerDocsVersion = '1.0.2' - micrometerVersion = '1.10.9' - micrometerTracingVersion = '1.0.8' + micrometerVersion = '1.10.10' + micrometerTracingVersion = '1.0.9' mockitoVersion = '4.8.1' rabbitmqStreamVersion = '0.8.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.16.1' - reactorVersion = '2022.0.9' + reactorVersion = '2022.0.10' snappyVersion = '1.1.8.4' springDataVersion = '2022.0.8' springRetryVersion = '2.0.2' From 8e645e2b2c32bd92d505daf099879c90210747cb Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 21 Aug 2023 22:25:58 +0000 Subject: [PATCH 275/737] [artifactory-release] Release version 3.0.8 --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index f44d96f2ea..31fee7544e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ -version=3.0.8-SNAPSHOT -org.gradle.caching=true +version=3.0.8 org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true +org.gradle.caching=true org.gradle.parallel=true kotlin.stdlib.default.dependency=false From c845cb7cc5d8d39224e3ea219fb3b005f35963f4 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 21 Aug 2023 22:26:00 +0000 Subject: [PATCH 276/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 31fee7544e..f770ff9cce 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.8 +version=3.0.9-SNAPSHOT org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true From 18c38a01259bb20d7b74a51a51fc2bfa6a7f79bc Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 19 Jul 2023 13:38:43 -0400 Subject: [PATCH 277/737] Use latest springAsciidocBackends from Central * Remove `org.asciidoctor.jvm.gems` plugin since it is out of use --- build.gradle | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index b5a5c9f1b8..6854f51054 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,6 @@ plugins { id 'io.spring.dependency-management' version '1.0.9.RELEASE' apply false id 'com.jfrog.artifactory' version '4.33.1' apply false id 'org.asciidoctor.jvm.pdf' version '3.3.2' - id 'org.asciidoctor.jvm.gems' version '3.3.2' id 'org.asciidoctor.jvm.convert' version '3.3.2' } @@ -38,7 +37,7 @@ ext { linkScmUrl = 'https://github.com/spring-projects/spring-amqp' linkScmConnection = 'git://github.com/spring-projects/spring-amqp.git' linkScmDevConnection = 'git@github.com:spring-projects/spring-amqp.git' - springAsciidoctorBackendsVersion = '0.0.3' + springAsciidoctorBackendsVersion = '0.0.7' modifiedFiles = files(grgit.status().unstaged.modified).filter{ f -> f.name.endsWith('.java') || f.name.endsWith('.kt') } From 31834589d727b697833ad5ef38117555dc9a848b Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 31 Aug 2023 16:46:57 -0400 Subject: [PATCH 278/737] Don't use `libs-milestone` & `libs-snapshot` Both those Spring repos require an authentication. Use regular https://repo.spring.io/milestone and https://repo.spring.io/snapshot instead. The rest of GA deps must be resolved from Maven Central --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 6854f51054..09db682fdb 100644 --- a/build.gradle +++ b/build.gradle @@ -110,9 +110,9 @@ allprojects { repositories { mavenCentral() - maven { url 'https://repo.spring.io/libs-milestone' } + maven { url 'https://repo.spring.io/milestone' } if (version.endsWith('-SNAPSHOT')) { - maven { url 'https://repo.spring.io/libs-snapshot' } + maven { url 'https://repo.spring.io/snapshot' } // maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } } // maven { url 'https://repo.spring.io/libs-staging-local' } From 48a793af3b948dd0529349bc11b46ac508cebfc9 Mon Sep 17 00:00:00 2001 From: Eric Haag Date: Fri, 1 Sep 2023 14:37:29 -0500 Subject: [PATCH 279/737] Relativize sourceDir property of asciidoctorPdf This sets the path sensitivity to relative for the sourceDir property of the asciidoctorPdf task. Previously, the full absolute path to the sources directory would be considered as an input to the task. This would cause a local build cache miss when executing the task from two different directories or a remote build cache hit given that the paths to the sources directory on CI and for local developers will undoubtedly be different. --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 09db682fdb..a823c54a25 100644 --- a/build.gradle +++ b/build.gradle @@ -587,7 +587,7 @@ asciidoctorPdf { asciidoctorj { sourceDir "$buildDir/asciidoc" - inputs.dir(sourceDir) + inputs.dir(sourceDir).withPathSensitivity(PathSensitivity.RELATIVE) sources { include 'index.adoc' } From e4caeb17cb31ab9deecc6f2afeb6a31d52219ca9 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 6 Sep 2023 13:39:54 -0700 Subject: [PATCH 280/737] Main to 3.1; Upgrade Dependency Versions Also compatibility changes for Mockito 5. * Upgrade Kotlin; fix test parameter names. --- build.gradle | 34 +++++++++---------- gradle.properties | 2 +- .../amqp/rabbit/AsyncRabbitTemplateTests.java | 11 ++++-- .../EnableRabbitIntegrationTests.java | 16 +++++---- .../MethodRabbitListenerEndpointTests.java | 4 +-- .../annotation-driven-full-config.xml | 2 +- ...tation-driven-full-configurable-config.xml | 2 +- .../annotation-driven-sample-config.xml | 2 +- .../ConnectionFactoryParserTests-context.xml | 3 +- .../config/ExchangeParserTests-context.xml | 3 +- .../QueueParserPlaceholderTests-context.xml | 3 +- .../config/QueueParserTests-context.xml | 3 +- .../config/TemplateParserTests-context.xml | 8 +++-- 13 files changed, 54 insertions(+), 39 deletions(-) diff --git a/build.gradle b/build.gradle index a823c54a25..5bf6330856 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlinVersion = '1.7.0' + ext.kotlinVersion = '1.9.10' ext.isCI = System.getenv('GITHUB_ACTION') || System.getenv('bamboo_buildKey') repositories { mavenCentral() @@ -42,7 +42,7 @@ ext { modifiedFiles = files(grgit.status().unstaged.modified).filter{ f -> f.name.endsWith('.java') || f.name.endsWith('.kt') } - assertjVersion = '3.23.1' + assertjVersion = '3.24.2' assertkVersion = '0.24' awaitilityVersion = '4.2.0' commonsCompressVersion = '1.20' @@ -51,26 +51,26 @@ ext { googleJsr305Version = '3.0.2' hamcrestVersion = '2.2' hibernateValidationVersion = '8.0.0.Final' - jacksonBomVersion = '2.14.2' - jaywayJsonPathVersion = '2.7.0' + jacksonBomVersion = '2.15.2' + jaywayJsonPathVersion = '2.8.0' junit4Version = '4.13.2' - junitJupiterVersion = '5.9.2' - kotlinCoroutinesVersion = '1.6.4' - log4jVersion = '2.19.0' - logbackVersion = '1.4.4' + junitJupiterVersion = '5.10.0' + kotlinCoroutinesVersion = '1.7.3' + log4jVersion = '2.20.0' + logbackVersion = '1.4.11' lz4Version = '1.8.0' micrometerDocsVersion = '1.0.2' - micrometerVersion = '1.10.10' - micrometerTracingVersion = '1.0.9' - mockitoVersion = '4.8.1' - rabbitmqStreamVersion = '0.8.0' - rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.16.1' - reactorVersion = '2022.0.10' + micrometerVersion = '1.12.0-SNAPSHOT' + micrometerTracingVersion = '1.2.0-SNAPSHOT' + mockitoVersion = '5.5.0' + rabbitmqStreamVersion = '0.12.0' + rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.18.0' + reactorVersion = '2023.0.0-SNAPSHOT' snappyVersion = '1.1.8.4' - springDataVersion = '2022.0.8' + springDataVersion = '2023.1.0-SNAPSHOT' springRetryVersion = '2.0.2' - springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.0.11' - testcontainersVersion = '1.17.6' + springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.1.0-SNAPSHOT' + testcontainersVersion = '1.19.0' zstdJniVersion = '1.5.0-2' javaProjects = subprojects - project(':spring-amqp-bom') diff --git a/gradle.properties b/gradle.properties index f770ff9cce..773310c3ba 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.0.9-SNAPSHOT +version=3.1.0-SNAPSHOT org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java index 039355ecea..b8a925f43d 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 the original author or authors. + * Copyright 2016-2023 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. @@ -56,6 +56,7 @@ import org.springframework.amqp.support.postprocessor.GZipPostProcessor; import org.springframework.amqp.utils.test.TestUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; @@ -528,12 +529,16 @@ public SimpleMessageListenerContainer replyContainer(ConnectionFactory connectio } @Bean - public AsyncRabbitTemplate asyncTemplate(RabbitTemplate template, SimpleMessageListenerContainer container) { + public AsyncRabbitTemplate asyncTemplate(@Qualifier("template") RabbitTemplate template, + SimpleMessageListenerContainer container) { + return new AsyncRabbitTemplate(template, container); } @Bean - public AsyncRabbitTemplate asyncDirectTemplate(RabbitTemplate templateForDirect) { + public AsyncRabbitTemplate asyncDirectTemplate( + @Qualifier("templateForDirect") RabbitTemplate templateForDirect) { + return new AsyncRabbitTemplate(templateForDirect); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java index 59808981ba..41a794f036 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java @@ -107,6 +107,7 @@ import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.ApplicationContext; @@ -409,7 +410,7 @@ public void multiListener() { this.rabbitTemplate.setAfterReceivePostProcessors(mpp); assertThat(rabbitTemplate.convertSendAndReceive("multi.exch", "multi.rk", qux)).isEqualTo("QUX: qux: multi.rk"); assertThat(beanMethodHeaders).hasSize(2); - assertThat(beanMethodHeaders.get(0)).contains("$MultiListenerBean"); + assertThat(beanMethodHeaders.get(0)).contains("MultiListenerBean"); assertThat(beanMethodHeaders.get(1)).isEqualTo("qux"); this.rabbitTemplate.removeAfterReceivePostProcessor(mpp); assertThat(rabbitTemplate.convertSendAndReceive("multi.exch.tx", "multi.rk.tx", bar)).isEqualTo("BAR: barbar"); @@ -1203,7 +1204,7 @@ public String multiQueuesConfig(String foo) { } @RabbitListener(queues = "test.header", group = "testGroup", replyPostProcessor = "#{'echoPrefixHeader'}") - public String capitalizeWithHeader(@Payload String content, @Header String prefix) { + public String capitalizeWithHeader(@Payload String content, @Header("prefix") String prefix) { return prefix + content.toUpperCase(); } @@ -1213,7 +1214,7 @@ public String capitalizeWithMessage(org.springframework.messaging.Message reply(String payload, @Header String foo, + public org.springframework.messaging.Message reply(String payload, @Header("foo") String foo, @Header(AmqpHeaders.CONSUMER_TAG) String tag) { return MessageBuilder.withPayload(payload) .setHeader("foo", foo).setHeader("bar", tag).build(); @@ -1677,7 +1678,9 @@ public SimpleRabbitListenerContainerFactory txListenerContainerFactory() { @Bean public SimpleMessageListenerContainer factoryCreatedContainerSimpleListener( + @Qualifier("rabbitListenerContainerFactory") SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory) { + SimpleRabbitListenerEndpoint listener = new SimpleRabbitListenerEndpoint(); listener.setQueueNames("test.manual.container"); listener.setMessageListener((ChannelAwareMessageListener) (message, channel) -> { @@ -1689,6 +1692,7 @@ public SimpleMessageListenerContainer factoryCreatedContainerSimpleListener( @Bean public SimpleMessageListenerContainer factoryCreatedContainerNoListener( + @Qualifier("rabbitListenerContainerFactory") SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory) { SimpleMessageListenerContainer container = rabbitListenerContainerFactory.createListenerContainer(); container.setMessageListener(message -> { @@ -1701,7 +1705,7 @@ public SimpleMessageListenerContainer factoryCreatedContainerNoListener( @Bean public SimpleRabbitListenerContainerFactory rabbitAutoStartFalseListenerContainerFactory( - ReplyPostProcessor rpp) { + @Qualifier("rpp") ReplyPostProcessor rpp) { SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); factory.setConnectionFactory(rabbitConnectionFactory()); @@ -2466,14 +2470,14 @@ public String messagingMessage(org.springframework.messaging.Message message) @RabbitListener(queues = "test.converted.foomessage") public String messagingMessage(org.springframework.messaging.Message message, - @Header(value = "", required = false) String h, + @Header(value = "notPresent", required = false) String h, @Header(name = AmqpHeaders.RECEIVED_USER_ID) String userId) { return message.getClass().getSimpleName() + message.getPayload().getClass().getSimpleName() + userId; } @RabbitListener(queues = "test.notconverted.messagingmessagenotgeneric") public String messagingMessage(@SuppressWarnings("rawtypes") org.springframework.messaging.Message message, - @Header(value = "", required = false) Integer h) { + @Header(value = "notPresent", required = false) Integer h) { return message.getClass().getSimpleName() + message.getPayload().getClass().getSimpleName(); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MethodRabbitListenerEndpointTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MethodRabbitListenerEndpointTests.java index 24463d19a3..87d4dbff83 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MethodRabbitListenerEndpointTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MethodRabbitListenerEndpointTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2023 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. @@ -465,7 +465,7 @@ public void resolveGenericMessage(Message message) { assertThat(message.getPayload()).as("Wrong message payload").isEqualTo("test"); } - public void resolveHeaderAndPayload(@Payload String content, @Header int myCounter, + public void resolveHeaderAndPayload(@Payload String content, @Header("myCounter") int myCounter, @Header(AmqpHeaders.CONSUMER_TAG) String tag, @Header(AmqpHeaders.CONSUMER_QUEUE) String queue) { invocations.put("resolveHeaderAndPayload", true); diff --git a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/annotation/annotation-driven-full-config.xml b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/annotation/annotation-driven-full-config.xml index 237a528ca1..a4993af914 100644 --- a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/annotation/annotation-driven-full-config.xml +++ b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/annotation/annotation-driven-full-config.xml @@ -14,7 +14,7 @@ - + diff --git a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/annotation/annotation-driven-full-configurable-config.xml b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/annotation/annotation-driven-full-configurable-config.xml index 21cc8555a3..c4d5336307 100644 --- a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/annotation/annotation-driven-full-configurable-config.xml +++ b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/annotation/annotation-driven-full-configurable-config.xml @@ -17,7 +17,7 @@ - + - + diff --git a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/ConnectionFactoryParserTests-context.xml b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/ConnectionFactoryParserTests-context.xml index 4ae8d756dc..b471046269 100644 --- a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/ConnectionFactoryParserTests-context.xml +++ b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/ConnectionFactoryParserTests-context.xml @@ -15,7 +15,8 @@ connection-name-strategy="connectionNameStrategy"/> - + diff --git a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/ExchangeParserTests-context.xml b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/ExchangeParserTests-context.xml index 616f63239f..a2004ce3c4 100644 --- a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/ExchangeParserTests-context.xml +++ b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/ExchangeParserTests-context.xml @@ -51,7 +51,8 @@ - + diff --git a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/QueueParserPlaceholderTests-context.xml b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/QueueParserPlaceholderTests-context.xml index f90c955380..41a47bfe93 100644 --- a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/QueueParserPlaceholderTests-context.xml +++ b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/QueueParserPlaceholderTests-context.xml @@ -87,7 +87,8 @@ - + diff --git a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/QueueParserTests-context.xml b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/QueueParserTests-context.xml index 676c05ef0e..4923ec7cfc 100644 --- a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/QueueParserTests-context.xml +++ b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/QueueParserTests-context.xml @@ -75,7 +75,8 @@ - + diff --git a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/TemplateParserTests-context.xml b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/TemplateParserTests-context.xml index cbc799a662..26b70d6de7 100644 --- a/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/TemplateParserTests-context.xml +++ b/spring-rabbit/src/test/resources/org/springframework/amqp/rabbit/config/TemplateParserTests-context.xml @@ -42,17 +42,19 @@ receive-connection-factory-selector-expression="'foo'"/> - + - + - + From f128c94d6ee62a14ed3f27229203900a43e5a278 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 6 Sep 2023 17:17:07 -0400 Subject: [PATCH 281/737] Upgrade HttpClient Version --- build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 5bf6330856..a672b98772 100644 --- a/build.gradle +++ b/build.gradle @@ -46,7 +46,7 @@ ext { assertkVersion = '0.24' awaitilityVersion = '4.2.0' commonsCompressVersion = '1.20' - commonsHttpClientVersion = '5.1.3' + commonsHttpClientVersion = '5.2.1' commonsPoolVersion = '2.11.1' googleJsr305Version = '3.0.2' hamcrestVersion = '2.2' @@ -447,6 +447,7 @@ project('spring-rabbit') { testImplementation 'io.micrometer:micrometer-tracing-integration-test' testImplementation "org.testcontainers:rabbitmq" testImplementation 'org.testcontainers:junit-jupiter' + testImplementation "org.apache.httpcomponents.client5:httpclient5:$commonsHttpClientVersion" testRuntimeOnly 'com.fasterxml.jackson.core:jackson-core' testRuntimeOnly 'com.fasterxml.jackson.core:jackson-databind' testRuntimeOnly 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' From 6f54c12c991db032c50cdd0e0a422f7df6c2d07a Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 7 Sep 2023 16:57:22 -0400 Subject: [PATCH 282/737] 3.1 What's New Doc --- src/reference/asciidoc/appendix.adoc | 62 +++++++++++++++++++++++++++ src/reference/asciidoc/whats-new.adoc | 57 +----------------------- 2 files changed, 63 insertions(+), 56 deletions(-) diff --git a/src/reference/asciidoc/appendix.adoc b/src/reference/asciidoc/appendix.adoc index 993fb899ec..60bee0b65a 100644 --- a/src/reference/asciidoc/appendix.adoc +++ b/src/reference/asciidoc/appendix.adoc @@ -29,6 +29,68 @@ See <>. [[previous-whats-new]] === Previous Releases +==== Changes in 3.0 Since 2.4 + +===== Java 17, Spring Framework 6.0 + +This version requires Spring Framework 6.0 and Java 17 + +===== Remoting + +The remoting feature (using RMI) is no longer supported. + +===== Observation + +Enabling observation for timers and tracing using Micrometer is now supported. +See <> for more information. + +[[x30-Native]] +===== Native Images + +Support for creating native images is provided. +See <> for more information. + +===== AsyncRabbitTemplate + +IMPORTANT: The `AsyncRabbitTemplate` now returns `CompletableFuture` s instead of `ListenableFuture` s. +See <> for more information. + +===== Stream Support Changes + +IMPORTANT: `RabbitStreamOperations` and `RabbitStreamTemplate` methods now return `CompletableFuture` instead of `ListenableFuture`. + +Super streams and single active consumers thereon are now supported. + +See <> 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 <> for more information. + +`MessageConverter` s can now return `Optional.empty()` for a null value; this is currently implemented by the `Jackson2JsonMessageConverter`. +See <> for more information + +You can now configure a `ReplyPostProcessor` via the container factory rather than via a property on `@RabbitListener`. +See <> for more information. + +The `@RabbitListener` (and `@RabbitHandler`) methods can now be declared as Kotlin `suspend` functions. +See <> for more information. + +Starting with version 3.0.5, listeners with async return types (including Kotlin suspend functions) invoke the `RabbitListenerErrorHandler` (if configured) after a failure. +Previously, the error handler was only invoked with synchronous invocations. + +===== Connection Factory Changes + +The default `addressShuffleMode` in `AbstractConnectionFactory` is now `RANDOM`. +This results in connecting to a random host when multiple addresses are provided. +See <> for more information. + +The `LocalizedQueueConnectionFactory` no longer uses the RabbitMQ `http-client` library to determine which node is the leader for a queue. +See <> for more information. + ==== Changes in 2.4 Since 2.3 This section describes the changes between version 2.3 and version 2.4. diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index d386714b5c..cea3c5a051 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -5,60 +5,5 @@ ==== Java 17, Spring Framework 6.0 -This version requires Spring Framework 6.0 and Java 17 +This version requires Spring Framework 6.1 and Java 17. -==== Remoting - -The remoting feature (using RMI) is no longer supported. - -==== Observation - -Enabling observation for timers and tracing using Micrometer is now supported. -See <> for more information. - -[[x30-Native]] -==== Native Images - -Support for creating native images is provided. -See <> for more information. - -==== AsyncRabbitTemplate - -IMPORTANT: The `AsyncRabbitTemplate` now returns `CompletableFuture` s instead of `ListenableFuture` s. -See <> for more information. - -==== Stream Support Changes - -IMPORTANT: `RabbitStreamOperations` and `RabbitStreamTemplate` methods now return `CompletableFuture` instead of `ListenableFuture`. - -Super streams and single active consumers thereon are now supported. - -See <> 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 <> for more information. - -`MessageConverter` s can now return `Optional.empty()` for a null value; this is currently implemented by the `Jackson2JsonMessageConverter`. -See <> for more information - -You can now configure a `ReplyPostProcessor` via the container factory rather than via a property on `@RabbitListener`. -See <> for more information. - -The `@RabbitListener` (and `@RabbitHandler`) methods can now be declared as Kotlin `suspend` functions. -See <> for more information. - -Starting with version 3.0.5, listeners with async return types (including Kotlin suspend functions) invoke the `RabbitListenerErrorHandler` (if configured) after a failure. -Previously, the error handler was only invoked with synchronous invocations. - -==== Connection Factory Changes - -The default `addressShuffleMode` in `AbstractConnectionFactory` is now `RANDOM`. -This results in connecting to a random host when multiple addresses are provided. -See <> for more information. - -The `LocalizedQueueConnectionFactory` no longer uses the RabbitMQ `http-client` library to determine which node is the leader for a queue. -See <> for more information. From 073062f9583e302637044e96a15754f32e59bb9d Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Thu, 7 Sep 2023 16:58:14 -0400 Subject: [PATCH 283/737] More 3.1 Docs --- src/reference/asciidoc/quick-tour.adoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/reference/asciidoc/quick-tour.adoc b/src/reference/asciidoc/quick-tour.adoc index 28a4a6bc4a..75029ba7ea 100644 --- a/src/reference/asciidoc/quick-tour.adoc +++ b/src/reference/asciidoc/quick-tour.adoc @@ -32,11 +32,11 @@ compile 'org.springframework.amqp:spring-rabbit:{project-version}' [[compatibility]] ===== Compatibility -The minimum Spring Framework version dependency is 5.2.0. +The minimum Spring Framework version dependency is 6.1.0. -The minimum `amqp-client` Java client library version is 5.7.0. +The minimum `amqp-client` Java client library version is 5.18.0. -The minimum `stream-client` Java client library for stream queues is 0.7.0. +The minimum `stream-client` Java client library for stream queues is 0.12.0. ===== Very, Very Quick From e3b860fd24495b7edf22fd1fa1c41573452550ab Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 11 Sep 2023 12:56:41 -0400 Subject: [PATCH 284/737] Revert Reactor to M2 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a672b98772..a5d581d6b2 100644 --- a/build.gradle +++ b/build.gradle @@ -65,7 +65,7 @@ ext { mockitoVersion = '5.5.0' rabbitmqStreamVersion = '0.12.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.18.0' - reactorVersion = '2023.0.0-SNAPSHOT' + reactorVersion = '2023.0.0-M2' snappyVersion = '1.1.8.4' springDataVersion = '2023.1.0-SNAPSHOT' springRetryVersion = '2.0.2' From 1ed63c9b42bd08f708967907cd26f7e2f1b43953 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 18 Sep 2023 09:56:14 -0400 Subject: [PATCH 285/737] Upgrade Spring, Data, Retry, Reactor, Micrometer (#2526) --- build.gradle | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index a5d581d6b2..e96358f563 100644 --- a/build.gradle +++ b/build.gradle @@ -60,16 +60,16 @@ ext { logbackVersion = '1.4.11' lz4Version = '1.8.0' micrometerDocsVersion = '1.0.2' - micrometerVersion = '1.12.0-SNAPSHOT' - micrometerTracingVersion = '1.2.0-SNAPSHOT' + micrometerVersion = '1.12.0-M3' + micrometerTracingVersion = '1.2.0-M3' mockitoVersion = '5.5.0' rabbitmqStreamVersion = '0.12.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.18.0' - reactorVersion = '2023.0.0-M2' + reactorVersion = '2023.0.0-M3' snappyVersion = '1.1.8.4' - springDataVersion = '2023.1.0-SNAPSHOT' - springRetryVersion = '2.0.2' - springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.1.0-SNAPSHOT' + springDataVersion = '2023.1.0-M3' + springRetryVersion = '2.0.3' + springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.1.0-M5' testcontainersVersion = '1.19.0' zstdJniVersion = '1.5.0-2' From 7a7579698ed567de801695ec4857eb43ff9d470f Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 18 Sep 2023 15:30:12 +0000 Subject: [PATCH 286/737] [artifactory-release] Release version 3.1.0-M1 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 773310c3ba..c6a0dc3491 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.1.0-SNAPSHOT +version=3.1.0-M1 org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true From b5ee282e251d30eae6679cb1988af67a2733d9a5 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 18 Sep 2023 15:30:13 +0000 Subject: [PATCH 287/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index c6a0dc3491..773310c3ba 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.1.0-M1 +version=3.1.0-SNAPSHOT org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true From 8e3dd19f63f873a77f96c2060b5c55ca1fbe2d7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Tid=C3=A9n?= Date: Tue, 19 Sep 2023 17:30:56 +0200 Subject: [PATCH 288/737] Fix typo in the CachingConnectionFactory Javadoc --- .../amqp/rabbit/connection/CachingConnectionFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java index dcf0dc3ce3..8ae4758796 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java @@ -84,7 +84,7 @@ *

* {@link CacheMode#CONNECTION} is not compatible with a Rabbit Admin that auto-declares queues etc. *

- * NOTE: This ConnectionFactory requires explicit closing of all Channels obtained form its Connection(s). + * NOTE: This ConnectionFactory requires explicit closing of all Channels obtained from its Connection(s). * This is the usual recommendation for native Rabbit access code anyway. However, with this ConnectionFactory, its use * is mandatory in order to actually allow for Channel reuse. {@link Channel#close()} returns the channel to the * cache, if there is room, or physically closes the channel otherwise. From 09c612c4babc2c4b78dd73458e3aab34500f4355 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 2 Oct 2023 12:18:12 -0400 Subject: [PATCH 289/737] Change Defaults **cherry-pick to 3.0.x, 2.4.x** --- build.gradle | 2 ++ .../amqp/utils/SerializationUtils.java | 23 +++++++++++++++---- ...istDeserializingMessageConverterTests.java | 21 ++++++++--------- src/reference/asciidoc/amqp.adoc | 3 ++- 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/build.gradle b/build.gradle index e96358f563..645fb6897c 100644 --- a/build.gradle +++ b/build.gradle @@ -313,6 +313,8 @@ configure(javaProjects) { subproject -> if (name ==~ /(testAll)/) { systemProperty 'RUN_LONG_INTEGRATION_TESTS', 'true' } + environment "SPRING_AMQP_DESERIALIZATION_TRUST_ALL", "true" + useJUnitPlatform() } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/utils/SerializationUtils.java b/spring-amqp/src/main/java/org/springframework/amqp/utils/SerializationUtils.java index d7e69f455e..89690df450 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/utils/SerializationUtils.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/utils/SerializationUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2006-2019 the original author or authors. + * Copyright 2006-2023 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. @@ -38,6 +38,17 @@ */ public final class SerializationUtils { + private static final String TRUST_ALL_ENV = "SPRING_AMQP_DESERIALIZATION_TRUST_ALL"; + + private static final String TRUST_ALL_PROP = "spring.amqp.deserialization.trust.all"; + + private static final boolean TRUST_ALL; + + static { + TRUST_ALL = Boolean.parseBoolean(System.getenv(TRUST_ALL_ENV)) + || Boolean.parseBoolean(System.getProperty(TRUST_ALL_PROP)); + } + private SerializationUtils() { } @@ -137,11 +148,12 @@ protected Class resolveClass(ObjectStreamClass classDesc) * @since 2.1 */ public static void checkAllowedList(Class clazz, Set patterns) { - if (ObjectUtils.isEmpty(patterns)) { + if (TRUST_ALL && ObjectUtils.isEmpty(patterns)) { return; } if (clazz.isArray() || clazz.isPrimitive() || clazz.equals(String.class) - || Number.class.isAssignableFrom(clazz)) { + || Number.class.isAssignableFrom(clazz) + || String.class.equals(clazz)) { return; } String className = clazz.getName(); @@ -150,7 +162,10 @@ public static void checkAllowedList(Class clazz, Set patterns) { return; } } - throw new SecurityException("Attempt to deserialize unauthorized " + clazz); + throw new SecurityException("Attempt to deserialize unauthorized " + clazz + + "; add allowed class name patterns to the message converter or, if you trust the message orginiator, " + + "set environment variable '" + + TRUST_ALL_ENV + "' or system property '" + TRUST_ALL_PROP + "' to true"); } } diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/AllowedListDeserializingMessageConverterTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/AllowedListDeserializingMessageConverterTests.java index dfb3d1709c..e56a568a16 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/AllowedListDeserializingMessageConverterTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/AllowedListDeserializingMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2023 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. @@ -17,7 +17,7 @@ package org.springframework.amqp.support.converter; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import java.io.Serializable; import java.util.Collections; @@ -40,7 +40,11 @@ public void testAllowedList() throws Exception { SerializerMessageConverter converter = new SerializerMessageConverter(); TestBean testBean = new TestBean("foo"); Message message = converter.toMessage(testBean, new MessageProperties()); - Object fromMessage = converter.fromMessage(message); + // when env var not set +// assertThatExceptionOfType(SecurityException.class).isThrownBy(() -> converter.fromMessage(message)); + Object fromMessage; + // when env var set. + fromMessage = converter.fromMessage(message); assertThat(fromMessage).isEqualTo(testBean); converter.setAllowedListPatterns(Collections.singletonList("*")); @@ -54,15 +58,8 @@ public void testAllowedList() throws Exception { fromMessage = converter.fromMessage(message); assertThat(fromMessage).isEqualTo(testBean); - try { - converter.setAllowedListPatterns(Collections.singletonList("foo.*")); - fromMessage = converter.fromMessage(message); - assertThat(fromMessage).isEqualTo(testBean); - fail("Expected SecurityException"); - } - catch (SecurityException e) { - - } + converter.setAllowedListPatterns(Collections.singletonList("foo.*")); + assertThatExceptionOfType(SecurityException.class).isThrownBy(() -> converter.fromMessage(message)); } @SuppressWarnings("serial") diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index 21c0adc2fd..202d47d704 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -4407,7 +4407,7 @@ consider configuring which packages and classes are allowed to be deserialized. This applies to both the `SimpleMessageConverter` and `SerializerMessageConverter` when it is configured to use a `DefaultDeserializer` either implicitly or via configuration. -By default, the allowed list is empty, meaning all classes are deserialized. +By default, the allowed list is empty, meaning no classes will be deserialized. You can set a list of patterns, such as `thing1.*`, `thing1.thing2.Cat` or `*.MySafeClass`. @@ -4415,6 +4415,7 @@ The patterns are checked in order until a match is found. If there is no match, a `SecurityException` is thrown. You can set the patterns using the `allowedListPatterns` property on these converters. +Alternatively, if you trust all message originators, you can set the environment variable `SPRING_AMQP_DESERIALIZATION_TRUST_ALL` or system property `spring.amqp.deserialization.trust.all` to `true`. ==== [[message-properties-converters]] From e521033dfdf5cdae909796ccd499d6e54cfbe891 Mon Sep 17 00:00:00 2001 From: hzhang <1049725878@qq.com> Date: Wed, 27 Sep 2023 09:38:31 +0800 Subject: [PATCH 290/737] GH-2528: Handle Conversion Exception with Batch Resolves https://github.com/spring-projects/spring-amqp/issues/2528 Detect a conversion exception within a batch and reject just that message. formatting code Polishing. --- .../BatchMessagingMessageListenerAdapter.java | 18 ++- ...hMessagingMessageListenerAdapterTests.java | 128 +++++++++++++++++- 2 files changed, 143 insertions(+), 3 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapter.java index 6667a7b94f..3bab4dd7f6 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-2023 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. @@ -25,6 +25,7 @@ import org.springframework.amqp.rabbit.listener.api.ChannelAwareBatchMessageListener; import org.springframework.amqp.rabbit.listener.api.RabbitListenerErrorHandler; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; +import org.springframework.amqp.support.converter.MessageConversionException; import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.support.GenericMessage; @@ -63,7 +64,20 @@ public void onMessageBatch(List messages, else { List> messagingMessages = new ArrayList<>(); for (org.springframework.amqp.core.Message message : messages) { - messagingMessages.add(toMessagingMessage(message)); + try { + Message messagingMessage = toMessagingMessage(message); + messagingMessages.add(messagingMessage); + } + catch (MessageConversionException e) { + this.logger.error("Could not convert incoming message", e); + try { + channel.basicReject(message.getMessageProperties().getDeliveryTag(), false); + } + catch (Exception ex) { + this.logger.error("Failed to reject message with conversion error", ex); + throw e; // NOSONAR + } + } } if (this.converterAdapter.isMessageList()) { converted = new GenericMessage<>(messagingMessages); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapterTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapterTests.java index 358c4676b4..d12f1d5a51 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapterTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2023 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. @@ -21,16 +21,42 @@ import java.lang.reflect.Method; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageBuilder; +import org.springframework.amqp.core.MessagePropertiesBuilder; +import org.springframework.amqp.rabbit.annotation.EnableRabbit; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.junit.RabbitAvailable; +import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; +import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory; +import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.amqp.utils.test.TestUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import com.fasterxml.jackson.databind.ObjectMapper; /** * @author Gary Russell + * @author heng zhang + * * @since 3.0 * */ +@SpringJUnitConfig +@RabbitAvailable(queues = "test.batchQueue") public class BatchMessagingMessageListenerAdapterTests { @Test @@ -52,4 +78,104 @@ public void listen(String in) { public void listen(List in) { } + + @Test + public void errorMsgConvert(@Autowired BatchMessagingMessageListenerAdapterTests.Config config, + @Autowired RabbitTemplate template) throws Exception { + + Message message = MessageBuilder.withBody(""" + { + "name" : "Tom", + "age" : 18 + } + """.getBytes()).andProperties( + MessagePropertiesBuilder.newInstance() + .setContentType("application/json") + .setReplyTo("nowhere") + .build()) + .build(); + + Message errorMessage = MessageBuilder.withBody("".getBytes()).andProperties( + MessagePropertiesBuilder.newInstance() + .setContentType("application/json") + .setReplyTo("nowhere") + .build()) + .build(); + + for (int i = 0; i < config.count; i++) { + template.send("test.batchQueue", message); + template.send("test.batchQueue", errorMessage); + } + + assertThat(config.countDownLatch.await(config.count * 1000L, TimeUnit.SECONDS)).isTrue(); + } + + + + @Configuration + @EnableRabbit + public static class Config { + volatile int count = 5; + volatile CountDownLatch countDownLatch = new CountDownLatch(count); + + @RabbitListener( + queues = "test.batchQueue", + containerFactory = "batchListenerContainerFactory" + ) + public void listen(List list) { + for (Model model : list) { + countDownLatch.countDown(); + } + + } + + @Bean + ConnectionFactory cf() { + return new CachingConnectionFactory(RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); + } + + @Bean(name = "batchListenerContainerFactory") + public RabbitListenerContainerFactory rc(ConnectionFactory connectionFactory) { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(connectionFactory); + factory.setPrefetchCount(1); + factory.setConcurrentConsumers(1); + factory.setBatchListener(true); + factory.setBatchSize(3); + factory.setConsumerBatchEnabled(true); + + Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter(new ObjectMapper()); + factory.setMessageConverter(jackson2JsonMessageConverter); + + return factory; + } + + @Bean + RabbitTemplate template(ConnectionFactory cf) { + return new RabbitTemplate(cf); + } + + + } + public static class Model { + String name; + String age; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAge() { + return age; + } + + public void setAge(String age) { + this.age = age; + } + } + } From f6d46f69b36cdbc3a0a3203518273b98cace9812 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 3 Oct 2023 10:58:32 -0400 Subject: [PATCH 291/737] GH-2532: Ignore Kotlin Continuation Parameter Resolves https://github.com/spring-projects/spring-amqp/issues/2532 The presence of the parameter caused the inferred type to be set to `null` due to ambiguous parameters present. It must be considered an ineligible type when inferring conversion types. **cherry-pick to 3.0.x (#2533), 2.4.x (#2534)** --- .../MessagingMessageListenerAdapter.java | 3 ++- .../annotation/EnableRabbitKotlinTests.kt | 18 ++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java index 473d2b5bd3..60a67e3075 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java @@ -447,7 +447,8 @@ private boolean isEligibleParameter(MethodParameter methodParameter) { Type parameterType = methodParameter.getGenericParameterType(); if (parameterType.equals(Channel.class) || parameterType.equals(MessageProperties.class) - || parameterType.equals(org.springframework.amqp.core.Message.class)) { + || parameterType.equals(org.springframework.amqp.core.Message.class) + || parameterType.getTypeName().startsWith("kotlin.coroutines.Continuation")) { return false; } if (parameterType instanceof ParameterizedType parameterizedType && diff --git a/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt b/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt index 2fc695faf2..8eadd20341 100644 --- a/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt +++ b/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt @@ -21,12 +21,15 @@ import assertk.assertions.isEqualTo import assertk.assertions.isTrue import org.junit.jupiter.api.Test import org.springframework.amqp.core.AcknowledgeMode +import org.springframework.amqp.core.MessageListener import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory import org.springframework.amqp.rabbit.connection.CachingConnectionFactory import org.springframework.amqp.rabbit.core.RabbitTemplate import org.springframework.amqp.rabbit.junit.RabbitAvailable import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition +import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry import org.springframework.amqp.rabbit.listener.api.RabbitListenerErrorHandler +import org.springframework.amqp.utils.test.TestUtils import org.springframework.aop.framework.ProxyFactory import org.springframework.beans.BeansException import org.springframework.beans.factory.annotation.Autowired @@ -57,17 +60,20 @@ class EnableRabbitKotlinTests { private lateinit var config: Config @Test - fun `send and wait for consume`() { + fun `send and wait for consume`(@Autowired registry: RabbitListenerEndpointRegistry) { val template = RabbitTemplate(this.config.cf()) template.convertAndSend("kotlinQueue", "test") - assertThat(this.config.latch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(this.config.latch.await(10, TimeUnit.SECONDS)).isTrue() + val listener = registry.getListenerContainer("single").messageListener + assertThat(TestUtils.getPropertyValue(listener, "messagingMessageConverter.inferredArgumentType").toString()) + .isEqualTo("class java.lang.String") } @Test fun `send and wait for consume with EH`() { val template = RabbitTemplate(this.config.cf()) template.convertAndSend("kotlinQueue1", "test") - assertThat(this.config.ehLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(this.config.ehLatch.await(10, TimeUnit.SECONDS)).isTrue() val reply = template.receiveAndConvert("kotlinReplyQueue", 10_000) assertThat(reply).isEqualTo("error processed"); } @@ -78,7 +84,7 @@ class EnableRabbitKotlinTests { val latch = CountDownLatch(1) - @RabbitListener(queues = ["kotlinQueue"]) + @RabbitListener(id = "single", queues = ["kotlinQueue"]) suspend fun handle(@Suppress("UNUSED_PARAMETER") data: String) { this.latch.countDown() } @@ -121,12 +127,12 @@ class EnableRabbitKotlinTests { } - @RabbitListener(queues = ["kotlinQueue1"], errorHandler = "#{eh}") + @RabbitListener(id = "multi", queues = ["kotlinQueue1"], errorHandler = "#{eh}") @SendTo("kotlinReplyQueue") open class Multi { @RabbitHandler - suspend fun handle(@Suppress("UNUSED_PARAMETER") data: String) { + fun handle(@Suppress("UNUSED_PARAMETER") data: String) { throw RuntimeException("fail") } From 4cf9b5c2c5b08f4dbca16c7d94c4e7ac618f3a04 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 4 Oct 2023 12:33:24 -0400 Subject: [PATCH 292/737] GH-2536: Exclusive Consumer Logging Improvements Resolves https://github.com/spring-projects/spring-amqp/issues/2536 Log messages due to access refused due to exclusive consumers at DEBUG level instead of WARN and INFO. * Use LogMessage to avoid enabled check. * Use LogMessage at INFO level too. --- .../support/ConditionalExceptionLogger.java | 14 +++++++++- .../connection/AbstractConnectionFactory.java | 11 +++----- .../amqp/rabbit/connection/RabbitUtils.java | 16 +++++++++++- .../AbstractMessageListenerContainer.java | 26 ++++--------------- .../DirectMessageListenerContainer.java | 20 +++++++------- .../SimpleMessageListenerContainer.java | 16 +++++++++--- ...ageListenerContainerIntegration2Tests.java | 21 ++++++++++----- src/reference/asciidoc/amqp.adoc | 17 ++++++++---- src/reference/asciidoc/whats-new.adoc | 12 +++++++-- 9 files changed, 94 insertions(+), 59 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/ConditionalExceptionLogger.java b/spring-amqp/src/main/java/org/springframework/amqp/support/ConditionalExceptionLogger.java index f6845d7c87..7d764fbada 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/ConditionalExceptionLogger.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/ConditionalExceptionLogger.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-2023 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. @@ -18,6 +18,8 @@ import org.apache.commons.logging.Log; +import org.springframework.core.log.LogMessage; + /** * For components that support customization of the logging of certain events, users can * provide an implementation of this interface to modify the existing logging behavior. @@ -37,4 +39,14 @@ public interface ConditionalExceptionLogger { */ void log(Log logger, String message, Throwable t); + /** + * Log a consumer restart; debug by default. + * @param logger the logger. + * @param message the message. + * @since 3.1 + */ + default void logRestart(Log logger, LogMessage message) { + logger.debug(message); + } + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java index 0735a691e7..312e7b20de 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java @@ -496,7 +496,7 @@ public void setConnectionNameStrategy(ConnectionNameStrategy connectionNameStrat * Set the strategy for logging close exceptions; by default, if a channel is closed due to a failed * passive queue declaration, it is logged at debug level. Normal channel closes (200 OK) are not * logged. All others are logged at ERROR level (unless access is refused due to an exclusive consumer - * condition, in which case, it is logged at INFO level). + * condition, in which case, it is logged at DEBUG level, since 3.1, previously INFO). * @param closeExceptionLogger the {@link ConditionalExceptionLogger}. * @since 1.5 */ @@ -720,10 +720,7 @@ public void handleUnblocked() { * close exceptions. * @since 1.5 */ - private static class DefaultChannelCloseLogger implements ConditionalExceptionLogger { - - DefaultChannelCloseLogger() { - } + public static class DefaultChannelCloseLogger implements ConditionalExceptionLogger { @Override public void log(Log logger, String message, Throwable t) { @@ -734,8 +731,8 @@ public void log(Log logger, String message, Throwable t) { } } else if (RabbitUtils.isExclusiveUseChannelClose(cause)) { - if (logger.isInfoEnabled()) { - logger.info(message + ": " + cause.getMessage()); + if (logger.isDebugEnabled()) { + logger.debug(message + ": " + cause.getMessage()); } } else if (!RabbitUtils.isNormalChannelClose(cause)) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java index 1b42b8c80a..768f5e66b8 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -409,4 +409,18 @@ public static SaslConfig stringToSaslConfig(String saslConfig, } } + /** + * Determine whether the exception is due to an access refused for an exclusive consumer. + * @param exception the exception. + * @return true if access refused. + * @since 3.1 + */ + public static boolean exclusiveAccesssRefused(Exception exception) { + return exception.getCause() instanceof IOException + && exception.getCause().getCause() instanceof ShutdownSignalException sse1 + && isExclusiveUseChannelClose(sse1) + || exception.getCause() instanceof ShutdownSignalException sse2 + && isExclusiveUseChannelClose(sse2); + } + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index 7392fdab3a..75a454dd3e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -91,7 +91,6 @@ import org.springframework.util.backoff.FixedBackOff; import com.rabbitmq.client.Channel; -import com.rabbitmq.client.ShutdownSignalException; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; @@ -1030,7 +1029,7 @@ public void setStatefulRetryFatalWithNullMessageId(boolean statefulRetryFatalWit /** * Set a {@link ConditionalExceptionLogger} for logging exclusive consumer failures. The - * default is to log such failures at WARN level. + * default is to log such failures at DEBUG level (since 3.1, previously WARN). * @param exclusiveConsumerExceptionLogger the conditional exception logger. * @since 1.5 */ @@ -2095,27 +2094,12 @@ protected WrappedTransactionException(Throwable cause) { * consumer failures. * @since 1.5 */ - private static class DefaultExclusiveConsumerLogger implements ConditionalExceptionLogger { - - DefaultExclusiveConsumerLogger() { - } + public static class DefaultExclusiveConsumerLogger implements ConditionalExceptionLogger { @Override - public void log(Log logger, String message, Throwable t) { - if (t instanceof ShutdownSignalException cause) { - if (RabbitUtils.isExclusiveUseChannelClose(cause)) { - if (logger.isWarnEnabled()) { - logger.warn(message + ": " + cause.toString()); - } - } - else if (!RabbitUtils.isNormalChannelClose(cause)) { - logger.error(message + ": " + cause.getMessage()); - } - } - else { - if (logger.isErrorEnabled()) { - logger.error("Unexpected invocation of " + getClass() + ", with message: " + message, t); - } + public void log(Log logger, String message, Throwable cause) { + if (logger.isDebugEnabled()) { + logger.debug(message + ": " + cause.toString()); } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java index 5eb2785318..ee3a02c7ad 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java @@ -782,24 +782,22 @@ private SimpleConsumer consume(String queue, int index, Connection connection) { @Nullable private SimpleConsumer handleConsumeException(String queue, int index, @Nullable SimpleConsumer consumerArg, - Exception e) { + Exception ex) { SimpleConsumer consumer = consumerArg; - if (e.getCause() instanceof ShutdownSignalException - && e.getCause().getMessage().contains("in exclusive use")) { - getExclusiveConsumerExceptionLogger().log(logger, - "Exclusive consumer failure", e.getCause()); - publishConsumerFailedEvent("Consumer raised exception, attempting restart", false, e); - } - else if (e.getCause() instanceof ShutdownSignalException - && RabbitUtils.isPassiveDeclarationChannelClose((ShutdownSignalException) e.getCause())) { + if (RabbitUtils.exclusiveAccesssRefused(ex)) { + getExclusiveConsumerExceptionLogger().log(logger, "Exclusive consumer failure", ex.getCause()); + publishConsumerFailedEvent("Consumer raised exception, attempting restart", false, ex); + } + else if (ex.getCause() instanceof ShutdownSignalException + && RabbitUtils.isPassiveDeclarationChannelClose((ShutdownSignalException) ex.getCause())) { publishMissingQueueEvent(queue); this.logger.error("Queue not present, scheduling consumer " - + (consumer == null ? "for queue " + queue : consumer) + " for restart", e); + + (consumer == null ? "for queue " + queue : consumer) + " for restart", ex); } else if (this.logger.isWarnEnabled()) { this.logger.warn("basicConsume failed, scheduling consumer " - + (consumer == null ? "for queue " + queue : consumer) + " for restart", e); + + (consumer == null ? "for queue " + queue : consumer) + " for restart", ex); } if (consumer == null) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java index 1eeec52810..af48868e5c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java @@ -16,7 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -57,6 +56,7 @@ import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.amqp.support.ConsumerTagStrategy; +import org.springframework.core.log.LogMessage; import org.springframework.jmx.export.annotation.ManagedMetric; import org.springframework.jmx.support.MetricType; import org.springframework.lang.Nullable; @@ -1165,6 +1165,8 @@ private final class AsyncMessageProcessingConsumer implements Runnable { private int consecutiveMessages; + private boolean failedExclusive; + AsyncMessageProcessingConsumer(BlockingQueueConsumer consumer) { this.consumer = consumer; @@ -1276,8 +1278,8 @@ public void run() { // NOSONAR - line count } } catch (AmqpIOException e) { - if (e.getCause() instanceof IOException && e.getCause().getCause() instanceof ShutdownSignalException - && e.getCause().getCause().getMessage().contains("in exclusive use")) { + if (RabbitUtils.exclusiveAccesssRefused(e)) { + this.failedExclusive = true; getExclusiveConsumerExceptionLogger().log(logger, "Exclusive consumer failure", e.getCause().getCause()); publishConsumerFailedEvent("Consumer raised exception, attempting restart", false, e); @@ -1460,7 +1462,13 @@ private void killOrRestart(boolean aborted) { } } else { - logger.info("Restarting " + this.consumer); + LogMessage restartMessage = LogMessage.of(() -> "Restarting " + this.consumer); + if (this.failedExclusive) { + getExclusiveConsumerExceptionLogger().logRestart(logger, restartMessage); + } + else { + logger.info(restartMessage); + } restart(this.consumer); } } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java index 0c247fc972..290b7d759d 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java @@ -19,7 +19,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.awaitility.Awaitility.with; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willAnswer; @@ -79,6 +78,7 @@ import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.log.LogMessage; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import com.rabbitmq.client.AMQP.Queue.DeclareOk; @@ -347,7 +347,7 @@ public void testListenFromAnonQueue() throws Exception { @Test public void testExclusive() throws Exception { Log logger = spy(TestUtils.getPropertyValue(this.template.getConnectionFactory(), "logger", Log.class)); - willReturn(true).given(logger).isInfoEnabled(); + willReturn(true).given(logger).isDebugEnabled(); new DirectFieldAccessor(this.template.getConnectionFactory()).setPropertyValue("logger", logger); CountDownLatch latch1 = new CountDownLatch(1000); SimpleMessageListenerContainer container1 = @@ -365,6 +365,7 @@ public void testExclusive() throws Exception { consumeLatch1.countDown(); } }); + container1.setBeanName("container1"); container1.afterPropertiesSet(); container1.start(); assertThat(consumeLatch1.await(10, TimeUnit.SECONDS)).isTrue(); @@ -386,9 +387,10 @@ else if (event instanceof ConsumeOkEvent) { consumeLatch2.countDown(); } }); + container2.setBeanName("container2"); container2.afterPropertiesSet(); Log containerLogger = spy(TestUtils.getPropertyValue(container2, "logger", Log.class)); - willReturn(true).given(containerLogger).isWarnEnabled(); + willReturn(true).given(containerLogger).isDebugEnabled(); new DirectFieldAccessor(container2).setPropertyValue("logger", containerLogger); container2.start(); for (int i = 0; i < 1000; i++) { @@ -404,13 +406,18 @@ else if (event instanceof ConsumeOkEvent) { } assertThat(latch2.await(10, TimeUnit.SECONDS)).isTrue(); container2.stop(); - ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - verify(logger, atLeastOnce()).info(captor.capture()); - assertThat(captor.getAllValues()).anyMatch(arg -> arg.contains("exclusive")); + ArgumentCaptor connLogCaptor = ArgumentCaptor.forClass(String.class); + verify(logger, atLeastOnce()).debug(connLogCaptor.capture()); + assertThat(connLogCaptor.getAllValues()).anyMatch(arg -> arg.contains("exclusive")); assertThat(eventRef.get().getReason()).isEqualTo("Consumer raised exception, attempting restart"); assertThat(eventRef.get().isFatal()).isFalse(); assertThat(eventRef.get().getThrowable()).isInstanceOf(AmqpIOException.class); - verify(containerLogger, atLeastOnce()).warn(any()); + ArgumentCaptor contLogCaptor = ArgumentCaptor.forClass(String.class); + verify(containerLogger, atLeastOnce()).debug(contLogCaptor.capture()); + assertThat(contLogCaptor.getAllValues()).anyMatch(arg -> arg.contains("exclusive")); + ArgumentCaptor lmCaptor = ArgumentCaptor.forClass(LogMessage.class); + verify(containerLogger).debug(lmCaptor.capture()); + assertThat(lmCaptor.getAllValues()).anyMatch(arg -> arg.toString().startsWith("Restarting ")); } @Test diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index 202d47d704..879cf2a0a9 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -940,17 +940,19 @@ See <> for one scenario where you might want to register a Version 1.5 introduced a mechanism to enable users to control logging levels. -The `CachingConnectionFactory` uses a default strategy to log channel closures as follows: +The `AbstractConnectionFactory` uses a default strategy to log channel closures as follows: * Normal channel closes (200 OK) are not logged. -* If a channel is closed due to a failed passive queue declaration, it is logged at debug level. +* If a channel is closed due to a failed passive queue declaration, it is logged at DEBUG level. * If a channel is closed because the `basic.consume` is refused due to an exclusive consumer condition, it is logged at -INFO level. +DEBUG level (since 3.1, previously INFO). * All others are logged at ERROR level. To modify this behavior, you can inject a custom `ConditionalExceptionLogger` into the `CachingConnectionFactory` in its `closeExceptionLogger` property. +Also, the `AbstractConnectionFactory.DefaultChannelCloseLogger` is now public, allowing it to be sub classed. + See also <>. [[runtime-cache-properties]] @@ -2364,8 +2366,13 @@ These events can be consumed by implementing `ApplicationListener>. Fatal errors are always logged at the `ERROR` level. diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index cea3c5a051..ce30779d1c 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -1,9 +1,17 @@ [[whats-new]] == What's New -=== Changes in 3.0 Since 2.4 +=== Changes in 3.1 Since 3.0 -==== Java 17, Spring Framework 6.0 +==== Java 17, Spring Framework 6.1 This version requires Spring Framework 6.1 and Java 17. +[[31-exc]] +==== Exclusive Consumer Logging + +Log messages reporting access refusal due to exclusive consumers are now logged at DEBUG level by default. +It remains possible to configure your own logging behavior by setting the `exclusiveConsumerExceptionLogger` and `closeExceptionLogger` properties on the listener container and connection factory respectively. +In addition, the `SimpleMessageListenerContainer` consumer restart after such an exception is now logged at DEBUG level by default (previously INFO). +A new method `logRestart()` has been added to the `ConditionalExceptionLogger` to allow this to be changed. +See <> and <> for more information. From 3620f11ebb773dc9bd0c7bce2e447164d4955cbe Mon Sep 17 00:00:00 2001 From: kurenchuksergey Date: Fri, 6 Oct 2023 00:00:49 +0200 Subject: [PATCH 293/737] GH-2540: Add SuperStreamBuilder Resolves https://github.com/spring-projects/spring-amqp/issues/2540 Usability improvements: New SuperStream builder builder provide a way to configure: - maxAge - maxLength - maxSegmentSize Usability improvements: New SuperStream builder License Usability improvements: New SuperStream builder Fix style tests and add a new one for the super stream builder Usability improvements: New SuperStream builder Covered x-initial-cluster-size. Fixes after review --- .../rabbit/stream/config/SuperStream.java | 44 ++++- .../stream/config/SuperStreamBuilder.java | 165 +++++++++++++++++ .../config/SuperStreamConfigurationTests.java | 166 ++++++++++++++++++ 3 files changed, 369 insertions(+), 6 deletions(-) create mode 100644 spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStreamBuilder.java create mode 100644 spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamConfigurationTests.java diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStream.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStream.java index 0d74183d79..6523ee8814 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStream.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStream.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2023 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. @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.BiFunction; @@ -36,8 +37,8 @@ * Create Super Stream Topology {@link Declarable}s. * * @author Gary Russell + * @author Sergei Kurenchuk * @since 3.0 - * */ public class SuperStream extends Declarables { @@ -47,9 +48,22 @@ public class SuperStream extends Declarables { * @param partitions the number of partitions. */ public SuperStream(String name, int partitions) { + this(name, partitions, Map.of()); + } + + /** + * Create a Super Stream with the provided parameters. + * @param name the stream name. + * @param partitions the number of partitions. + * @param arguments the stream arguments + * @since 3.1 + */ + public SuperStream(String name, int partitions, Map arguments) { this(name, partitions, (q, i) -> IntStream.range(0, i) .mapToObj(String::valueOf) - .collect(Collectors.toList())); + .collect(Collectors.toList()), + arguments + ); } /** @@ -61,19 +75,37 @@ public SuperStream(String name, int partitions) { * partitions, the returned list must have a size equal to the partitions. */ public SuperStream(String name, int partitions, BiFunction> routingKeyStrategy) { - super(declarables(name, partitions, routingKeyStrategy)); + this(name, partitions, routingKeyStrategy, Map.of()); + } + + /** + * Create a Super Stream with the provided parameters. + * @param name the stream name. + * @param partitions the number of partitions. + * @param routingKeyStrategy a strategy to determine routing keys to use for the + * partitions. The first parameter is the queue name, the second the number of + * partitions, the returned list must have a size equal to the partitions. + * @param arguments the stream arguments + * @since 3.1 + */ + public SuperStream(String name, int partitions, BiFunction> routingKeyStrategy, Map arguments) { + super(declarables(name, partitions, routingKeyStrategy, arguments)); } private static Collection declarables(String name, int partitions, - BiFunction> routingKeyStrategy) { + BiFunction> routingKeyStrategy, + Map arguments) { List declarables = new ArrayList<>(); List rks = routingKeyStrategy.apply(name, partitions); Assert.state(rks.size() == partitions, () -> "Expected " + partitions + " routing keys, not " + rks.size()); declarables.add(new DirectExchange(name, true, false, Map.of("x-super-stream", true))); + + Map argumentsCopy = new HashMap<>(arguments); + argumentsCopy.put("x-queue-type", "stream"); for (int i = 0; i < partitions; i++) { String rk = rks.get(i); - Queue q = new Queue(name + "-" + i, true, false, false, Map.of("x-queue-type", "stream")); + Queue q = new Queue(name + "-" + i, true, false, false, argumentsCopy); declarables.add(q); declarables.add(new Binding(q.getName(), DestinationType.QUEUE, name, rk, Map.of("x-stream-partition-order", i))); diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStreamBuilder.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStreamBuilder.java new file mode 100644 index 0000000000..b2a4b574a7 --- /dev/null +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStreamBuilder.java @@ -0,0 +1,165 @@ +/* + * Copyright 2021-2023 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.rabbit.stream.config; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; + +import org.springframework.util.StringUtils; + +/** + * Builds a Spring AMQP Super Stream using a fluent API. + * Based on Streams documentation + * + * @author Sergei Kurenchuk + * @since 3.1 + */ +public class SuperStreamBuilder { + private final Map arguments = new HashMap<>(); + private String name; + private int partitions = -1; + + private BiFunction> routingKeyStrategy; + + /** + * Creates a builder for Super Stream. + * @param name stream name + * @return the builder + */ + public static SuperStreamBuilder superStream(String name) { + SuperStreamBuilder builder = new SuperStreamBuilder(); + builder.name(name); + return builder; + } + + /** + * Creates a builder for Super Stream. + * @param name stream name + * @param partitions partitions number + * @return the builder + */ + public static SuperStreamBuilder superStream(String name, int partitions) { + return superStream(name).partitions(partitions); + } + + /** + * Set the maximum age retention per stream, which will remove the oldest data. + * @param maxAge valid units: Y, M, D, h, m, s. For example: "7D" for a week + * @return the builder + */ + public SuperStreamBuilder maxAge(String maxAge) { + return withArgument("x-max-age", maxAge); + } + + /** + * Set the maximum log size as the retention configuration for each stream, + * which will truncate the log based on the data size. + * @param bytes the max total size in bytes + * @return the builder + */ + public SuperStreamBuilder maxLength(int bytes) { + return withArgument("max-length-bytes", bytes); + } + + /** + * Set the maximum size limit for segment file. + * @param bytes the max segments size in bytes + * @return the builder + */ + public SuperStreamBuilder maxSegmentSize(int bytes) { + return withArgument("x-stream-max-segment-size-bytes", bytes); + } + + /** + * Set initial replication factor for each partition. + * @param count number of nodes per partition + * @return the builder + */ + public SuperStreamBuilder initialClusterSize(int count) { + return withArgument("x-initial-cluster-size", count); + } + + /** + * Set extra argument which is not covered by builder's methods. + * @param key argument name + * @param value argument value + * @return the builder + */ + public SuperStreamBuilder withArgument(String key, Object value) { + if ("x-queue-type".equals(key) && !"stream".equals(value)) { + throw new IllegalArgumentException("Changing x-queue-type argument is not permitted"); + } + this.arguments.put(key, value); + return this; + } + + /** + * Set the stream name. + * @param name the stream name. + * @return the builder + */ + public SuperStreamBuilder name(String name) { + this.name = name; + return this; + } + + /** + * Set the partitions number. + * @param partitions the partitions number + * @return the builder + */ + public SuperStreamBuilder partitions(int partitions) { + this.partitions = partitions; + return this; + } + + /** + * Set a strategy to determine routing keys to use for the + * partitions. The first parameter is the queue name, the second the number of + * partitions, the returned list must have a size equal to the partitions. + * @param routingKeyStrategy the strategy + * @return the builder + */ + public SuperStreamBuilder routingKeyStrategy(BiFunction> routingKeyStrategy) { + this.routingKeyStrategy = routingKeyStrategy; + return this; + } + + /** + * Builds a final Super Stream. + * @return the Super Stream instance + */ + public SuperStream build() { + if (!StringUtils.hasText(this.name)) { + throw new IllegalArgumentException("Stream name can't be empty"); + } + + if (this.partitions <= 0) { + throw new IllegalArgumentException( + String.format("Partitions number should be great then zero. Current value; %d", this.partitions) + ); + } + + if (this.routingKeyStrategy == null) { + return new SuperStream(this.name, this.partitions, this.arguments); + } + + return new SuperStream(this.name, this.partitions, this.routingKeyStrategy, this.arguments); + } +} diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamConfigurationTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamConfigurationTests.java new file mode 100644 index 0000000000..781f1fa97d --- /dev/null +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamConfigurationTests.java @@ -0,0 +1,166 @@ +/* + * Copyright 2021-2023 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.rabbit.stream.config; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.Queue; + +/** + * @author Sergei Kurenchuk + * @since 3.1 + */ +public class SuperStreamConfigurationTests { + + @Test + void argumentsShouldBeAppliedToAllPartitions() { + int partitions = 3; + var argKey = "x-max-age"; + var argValue = 10_000; + + Map testArguments = Map.of(argKey, argValue); + SuperStream superStream = new SuperStream("stream", partitions, testArguments); + + List streams = superStream.getDeclarablesByType(Queue.class); + Assertions.assertEquals(partitions, streams.size()); + + streams.forEach( + it -> { + Object value = it.getArguments().get(argKey); + Assertions.assertNotNull(value, "Arg value should be present"); + Assertions.assertEquals(argValue, value, "Value should be the same"); + } + ); + } + + @Test + void testCustomPartitionsRoutingStrategy() { + var streamName = "test-super-stream-name"; + var partitions = 3; + var names = List.of("test.stream.1", "test.stream.2", "test.stream.3"); + + SuperStream superStream = SuperStreamBuilder.superStream(streamName, partitions) + .routingKeyStrategy((name, partition) -> names) + .build(); + + List bindings = superStream.getDeclarablesByType(Binding.class); + Set routingKeys = bindings.stream().map(Binding::getRoutingKey).collect(Collectors.toSet()); + Assertions.assertTrue(routingKeys.containsAll(names)); + } + + @Test + void builderMustSetupNameAndPartitionsNumber() { + var name = "test-super-stream-name"; + var partitions = 3; + SuperStream superStream = SuperStreamBuilder.superStream(name, partitions).build(); + List streams = superStream.getDeclarablesByType(Queue.class); + Assertions.assertEquals(partitions, streams.size()); + + streams.forEach(it -> Assertions.assertTrue(it.getName().startsWith(name))); + } + + @Test + void builderMustSetupArguments() { + var finalPartitionsNumber = 4; + var finalName = "test-name"; + var maxAge = "1D"; + var maxLength = 10_000_000; + var maxSegmentsSize = 100_000; + var initialClusterSize = 5; + + var testArgName = "test-key"; + var testArgValue = "test-value"; + + SuperStream superStream = SuperStreamBuilder.superStream("name", 3) + .partitions(finalPartitionsNumber) + .maxAge(maxAge) + .maxLength(maxLength) + .maxSegmentSize(maxSegmentsSize) + .initialClusterSize(initialClusterSize) + .name(finalName) + .withArgument(testArgName, testArgValue) + .build(); + + List streams = superStream.getDeclarablesByType(Queue.class); + + Assertions.assertEquals(finalPartitionsNumber, streams.size()); + streams.forEach( + it -> { + Assertions.assertTrue(it.getName().startsWith(finalName)); + Assertions.assertEquals(maxAge, it.getArguments().get("x-max-age")); + Assertions.assertEquals(maxLength, it.getArguments().get("max-length-bytes")); + Assertions.assertEquals(initialClusterSize, it.getArguments().get("x-initial-cluster-size")); + Assertions.assertEquals(maxSegmentsSize, it.getArguments().get("x-stream-max-segment-size-bytes")); + Assertions.assertEquals(testArgValue, it.getArguments().get(testArgName)); + } + ); + } + + @Test + void builderShouldForbidInternalArgumentsChanges() { + SuperStreamBuilder builder = SuperStreamBuilder.superStream("name", 3); + + Assertions.assertThrows(IllegalArgumentException.class, () -> builder.withArgument("x-queue-type", "quorum")); + } + + @Test + void nameCantBeEmpty() { + Assertions.assertThrows( + IllegalArgumentException.class, + () -> SuperStreamBuilder.superStream("", 3).build() + ); + + Assertions.assertThrows( + IllegalArgumentException.class, + () -> SuperStreamBuilder.superStream("testName", 3).name("").build() + ); + + Assertions.assertDoesNotThrow( + () -> SuperStreamBuilder.superStream("testName", 3).build() + ); + } + + @Test + void partitionsNumberShouldBeGreatThenZero() { + Assertions.assertThrows( + IllegalArgumentException.class, + () -> SuperStreamBuilder.superStream("testName", 0).build() + ); + + Assertions.assertThrows( + IllegalArgumentException.class, + () -> SuperStreamBuilder.superStream("testName", -1).build() + ); + + Assertions.assertThrows( + IllegalArgumentException.class, + () -> SuperStreamBuilder.superStream("testName", 1).partitions(0).build() + ); + + Assertions.assertDoesNotThrow( + () -> SuperStreamBuilder.superStream("testName", 1).build() + ); + } + +} From 8923a08ef1dac3364156e896393c158fc96d46d4 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 16 Oct 2023 10:19:21 -0400 Subject: [PATCH 294/737] Upgrade Spring, Data, Retry, Reactor, Micrometer (#2545) --- build.gradle | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index 645fb6897c..af995fe59c 100644 --- a/build.gradle +++ b/build.gradle @@ -60,16 +60,16 @@ ext { logbackVersion = '1.4.11' lz4Version = '1.8.0' micrometerDocsVersion = '1.0.2' - micrometerVersion = '1.12.0-M3' - micrometerTracingVersion = '1.2.0-M3' + micrometerVersion = '1.12.0-RC1' + micrometerTracingVersion = '1.2.0-RC1' mockitoVersion = '5.5.0' rabbitmqStreamVersion = '0.12.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.18.0' - reactorVersion = '2023.0.0-M3' + reactorVersion = '2023.0.0-RC1' snappyVersion = '1.1.8.4' - springDataVersion = '2023.1.0-M3' - springRetryVersion = '2.0.3' - springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.1.0-M5' + springDataVersion = '2023.1.0-RC1' + springRetryVersion = '2.0.4' + springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.1.0-RC1' testcontainersVersion = '1.19.0' zstdJniVersion = '1.5.0-2' From 7fdc55d6c0f14ffbcb3f71c85c86a79e5d7a466b Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 16 Oct 2023 15:06:46 +0000 Subject: [PATCH 295/737] [artifactory-release] Release version 3.1.0-RC1 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 773310c3ba..8cbd26a3cb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.1.0-SNAPSHOT +version=3.1.0-RC1 org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true From 80313264e9dfc63c75ebd43d2dbeb6aa63f22c23 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 16 Oct 2023 15:06:48 +0000 Subject: [PATCH 296/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 8cbd26a3cb..773310c3ba 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.1.0-RC1 +version=3.1.0-SNAPSHOT org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true From 1b38f94941be872b7bcabca99723ebf9b3731191 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Wed, 25 Oct 2023 09:48:16 -0400 Subject: [PATCH 297/737] GH-2546: Fix Super Stream Example in Docs (#2547) Resolves https://github.com/spring-projects/spring-amqp/issues/2546 Incorrectly added prototype scope to the bean (copy/paste from test). --- src/reference/asciidoc/stream.adoc | 1 - 1 file changed, 1 deletion(-) diff --git a/src/reference/asciidoc/stream.adoc b/src/reference/asciidoc/stream.adoc index ba3e844fdb..a53c2e4889 100644 --- a/src/reference/asciidoc/stream.adoc +++ b/src/reference/asciidoc/stream.adoc @@ -295,7 +295,6 @@ Invoke the `superStream` method on the listener container to enable a single act [source, java] ---- @Bean -@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) StreamListenerContainer container(Environment env, String name) { StreamListenerContainer container = new StreamListenerContainer(env); container.superStream("ss.sac", "myConsumer", 3); // concurrency = 3 From 613c2895abea15f3be771ff4048a4e5ee6d1fc6b Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 31 Oct 2023 11:51:18 -0400 Subject: [PATCH 298/737] GH-2552: Fix Builder Argument Types maxLength etc Resolves https://github.com/spring-projects/spring-amqp/issues/2552 Variables map to Erlang ints which are 8 bytes, so need to be long in Java. --- .../springframework/amqp/core/QueueBuilder.java | 16 +++++++++++++++- .../rabbit/stream/config/SuperStreamBuilder.java | 5 +++-- .../config/SuperStreamConfigurationTests.java | 4 ++-- .../core/FixedReplyQueueDeadLetterTests.java | 8 ++++---- ...mplatePublisherCallbacksIntegrationTests.java | 4 ++-- ...ageRecovererWithConfirmsIntegrationTests.java | 4 ++-- 6 files changed, 28 insertions(+), 13 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/QueueBuilder.java b/spring-amqp/src/main/java/org/springframework/amqp/core/QueueBuilder.java index 0b99462d35..122a7ea11d 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/QueueBuilder.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/QueueBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 the original author or authors. + * Copyright 2016-2023 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. @@ -150,12 +150,26 @@ public QueueBuilder expires(int expires) { * @param count the number of (ready) messages allowed. * @return the builder. * @since 2.2 + * @deprecated in favor of {@link #maxLength(long)}. * @see #overflow(Overflow) */ + @Deprecated public QueueBuilder maxLength(int count) { return withArgument("x-max-length", count); } + /** + * Set the number of (ready) messages allowed in the queue before it starts to drop + * them. + * @param count the number of (ready) messages allowed. + * @return the builder. + * @since 3.1 + * @see #overflow(Overflow) + */ + public QueueBuilder maxLength(long count) { + return withArgument("x-max-length", count); + } + /** * Set the total aggregate body size allowed in the queue before it starts to drop * them. diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStreamBuilder.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStreamBuilder.java index b2a4b574a7..cbe565e80a 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStreamBuilder.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStreamBuilder.java @@ -28,6 +28,7 @@ * Based on Streams documentation * * @author Sergei Kurenchuk + * @author Gary Russell * @since 3.1 */ public class SuperStreamBuilder { @@ -73,7 +74,7 @@ public SuperStreamBuilder maxAge(String maxAge) { * @param bytes the max total size in bytes * @return the builder */ - public SuperStreamBuilder maxLength(int bytes) { + public SuperStreamBuilder maxLength(long bytes) { return withArgument("max-length-bytes", bytes); } @@ -82,7 +83,7 @@ public SuperStreamBuilder maxLength(int bytes) { * @param bytes the max segments size in bytes * @return the builder */ - public SuperStreamBuilder maxSegmentSize(int bytes) { + public SuperStreamBuilder maxSegmentSize(long bytes) { return withArgument("x-stream-max-segment-size-bytes", bytes); } diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamConfigurationTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamConfigurationTests.java index 781f1fa97d..bb0fd60180 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamConfigurationTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamConfigurationTests.java @@ -85,8 +85,8 @@ void builderMustSetupArguments() { var finalPartitionsNumber = 4; var finalName = "test-name"; var maxAge = "1D"; - var maxLength = 10_000_000; - var maxSegmentsSize = 100_000; + var maxLength = 10_000_000L; + var maxSegmentsSize = 100_000L; var initialClusterSize = 5; var testArgName = "test-key"; diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/FixedReplyQueueDeadLetterTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/FixedReplyQueueDeadLetterTests.java index f4dbce98ad..b132f42d1b 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/FixedReplyQueueDeadLetterTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/FixedReplyQueueDeadLetterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2023 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. @@ -265,7 +265,7 @@ public Queue allArgs1() { return QueueBuilder.nonDurable("all.args.1") .ttl(1000) .expires(200_000) - .maxLength(42) + .maxLength(42L) .maxLengthBytes(10_000) .overflow(Overflow.rejectPublish) .deadLetterExchange("reply.dlx") @@ -282,7 +282,7 @@ public Queue allArgs2() { return QueueBuilder.nonDurable("all.args.2") .ttl(1000) .expires(200_000) - .maxLength(42) + .maxLength(42L) .maxLengthBytes(10_000) .overflow(Overflow.dropHead) .deadLetterExchange("reply.dlx") @@ -298,7 +298,7 @@ public Queue allArgs3() { return QueueBuilder.nonDurable("all.args.3") .ttl(1000) .expires(200_000) - .maxLength(42) + .maxLength(42L) .maxLengthBytes(10_000) .overflow(Overflow.rejectPublish) .deadLetterExchange("reply.dlx") diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests.java index 2c9808fefe..e8c4150215 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -833,7 +833,7 @@ public void testWithFuture() throws Exception { RabbitAdmin admin = new RabbitAdmin(this.connectionFactory); Queue queue = QueueBuilder.nonDurable() .autoDelete() - .maxLength(1) + .maxLength(1L) .overflow(Overflow.rejectPublish) .build(); admin.declareQueue(queue); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirmsIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirmsIntegrationTests.java index dae6bf2ed4..fff73934ab 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirmsIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirmsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2023 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. @@ -103,7 +103,7 @@ void testCorrelatedWithNack() { RabbitTemplate template = new RabbitTemplate(ccf); RabbitAdmin admin = new RabbitAdmin(ccf); Queue queue = QueueBuilder.durable(QUEUE + ".nack") - .maxLength(1) + .maxLength(1L) .overflow(Overflow.rejectPublish) .build(); admin.declareQueue(queue); From 761be85a68e83bf8ccdf9e014df08b7a1ae79461 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 2 Oct 2023 12:18:12 -0400 Subject: [PATCH 299/737] GH-2522: Upgrade to Rabbit Streams `0.14.0` Fixes https://github.com/spring-projects/spring-amqp/issues/2522 The latest `com.rabbitmq:stream-client` has some convenient fixes for local environment development (including Docker image). Therefore, fix all the Streams test to use just `.port(streamPort())` option for the `Environment` instead of custom `addressResolver` --- build.gradle | 2 +- .../rabbit/stream/listener/RabbitListenerTests.java | 6 +++--- .../stream/listener/SuperStreamConcurrentSACTests.java | 6 +++--- .../rabbit/stream/listener/SuperStreamSACTests.java | 6 +++--- .../rabbit/stream/micrometer/TracingTests.java | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/build.gradle b/build.gradle index af995fe59c..71016c2622 100644 --- a/build.gradle +++ b/build.gradle @@ -63,7 +63,7 @@ ext { micrometerVersion = '1.12.0-RC1' micrometerTracingVersion = '1.2.0-RC1' mockitoVersion = '5.5.0' - rabbitmqStreamVersion = '0.12.0' + rabbitmqStreamVersion = '0.14.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.18.0' reactorVersion = '2023.0.0-RC1' snappyVersion = '1.1.8.4' diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java index f1d847d849..c418c0ecfd 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java @@ -61,7 +61,6 @@ import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.util.UriUtils; -import com.rabbitmq.stream.Address; import com.rabbitmq.stream.Environment; import com.rabbitmq.stream.Message; import com.rabbitmq.stream.MessageHandler.Context; @@ -75,6 +74,7 @@ /** * @author Gary Russell + * @author Artem Bilan * @since 2.4 * */ @@ -203,7 +203,7 @@ ObservationRegistry obsReg(MeterRegistry meterRegistry) { @Bean static Environment environment() { return Environment.builder() - .addressResolver(add -> new Address("localhost", streamPort())) + .port(streamPort()) .build(); } @@ -395,6 +395,6 @@ Queue queue() { .build(); } - } + } } diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamConcurrentSACTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamConcurrentSACTests.java index 40e7fdbada..c7d34dd83e 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamConcurrentSACTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamConcurrentSACTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2023 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. @@ -39,12 +39,12 @@ import org.springframework.rabbit.stream.config.SuperStream; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import com.rabbitmq.stream.Address; import com.rabbitmq.stream.Environment; import com.rabbitmq.stream.OffsetSpecification; /** * @author Gary Russell + * @author Artem Bilan * @since 3.0 * */ @@ -105,7 +105,7 @@ SuperStream superStream() { @Bean static Environment environment() { return Environment.builder() - .addressResolver(add -> new Address("localhost", streamPort())) + .port(streamPort()) .maxConsumersByConnection(1) .build(); } diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java index 595e18cb5b..1daaca2d33 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2023 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. @@ -47,12 +47,12 @@ import org.springframework.rabbit.stream.config.SuperStream; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import com.rabbitmq.stream.Address; import com.rabbitmq.stream.Environment; import com.rabbitmq.stream.OffsetSpecification; /** * @author Gary Russell + * @author Artem Bilan * @since 3.0 * */ @@ -122,7 +122,7 @@ SuperStream superStream() { @Bean static Environment environment() { return Environment.builder() - .addressResolver(add -> new Address("localhost", streamPort())) + .port(streamPort()) .build(); } diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/micrometer/TracingTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/micrometer/TracingTests.java index bd93149e61..a90eb4ac7c 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/micrometer/TracingTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/micrometer/TracingTests.java @@ -38,7 +38,6 @@ import org.springframework.rabbit.stream.producer.RabbitStreamTemplate; import org.springframework.rabbit.stream.support.StreamAdmin; -import com.rabbitmq.stream.Address; import com.rabbitmq.stream.Environment; import com.rabbitmq.stream.Message; import com.rabbitmq.stream.OffsetSpecification; @@ -51,6 +50,7 @@ /** * @author Gary Russell + * @author Artem Bilan * @since 3.0.5 * */ @@ -106,7 +106,7 @@ public static class Config { @Bean static Environment environment() { return Environment.builder() - .addressResolver(add -> new Address("localhost", AbstractTestContainerTests.streamPort())) + .port(AbstractTestContainerTests.streamPort()) .build(); } From 1daec6bd7dfb7b2072bc597afabefd39f8099dd5 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 20 Nov 2023 09:09:41 -0500 Subject: [PATCH 300/737] Upgrade Dependency Versions (#2557) - assertk - Commons pool - Hibernate validation - Jackson - Log4j - Micrometer - Micrometer Tracing - Mockito - AMQP client - Reactor - Spring Data - Spring Framework - TestContainers --- build.gradle | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/build.gradle b/build.gradle index 71016c2622..499c81ccbc 100644 --- a/build.gradle +++ b/build.gradle @@ -43,34 +43,34 @@ ext { files(grgit.status().unstaged.modified).filter{ f -> f.name.endsWith('.java') || f.name.endsWith('.kt') } assertjVersion = '3.24.2' - assertkVersion = '0.24' + assertkVersion = '0.27.0' awaitilityVersion = '4.2.0' commonsCompressVersion = '1.20' commonsHttpClientVersion = '5.2.1' - commonsPoolVersion = '2.11.1' + commonsPoolVersion = '2.12.0' googleJsr305Version = '3.0.2' hamcrestVersion = '2.2' - hibernateValidationVersion = '8.0.0.Final' - jacksonBomVersion = '2.15.2' + hibernateValidationVersion = '8.0.1.Final' + jacksonBomVersion = '2.15.3' jaywayJsonPathVersion = '2.8.0' junit4Version = '4.13.2' junitJupiterVersion = '5.10.0' kotlinCoroutinesVersion = '1.7.3' - log4jVersion = '2.20.0' + log4jVersion = '2.21.1' logbackVersion = '1.4.11' lz4Version = '1.8.0' micrometerDocsVersion = '1.0.2' - micrometerVersion = '1.12.0-RC1' - micrometerTracingVersion = '1.2.0-RC1' - mockitoVersion = '5.5.0' + micrometerVersion = '1.12.0' + micrometerTracingVersion = '1.2.0' + mockitoVersion = '5.6.0' rabbitmqStreamVersion = '0.14.0' - rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.18.0' - reactorVersion = '2023.0.0-RC1' + rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.19.0' + reactorVersion = '2023.0.0' snappyVersion = '1.1.8.4' - springDataVersion = '2023.1.0-RC1' + springDataVersion = '2023.1.0' springRetryVersion = '2.0.4' - springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.1.0-RC1' - testcontainersVersion = '1.19.0' + springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.1.0' + testcontainersVersion = '1.19.1' zstdJniVersion = '1.5.0-2' javaProjects = subprojects - project(':spring-amqp-bom') From 1f62b1dc471969b3df9a075044b425d2882d0637 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 20 Nov 2023 16:43:03 +0000 Subject: [PATCH 301/737] [artifactory-release] Release version 3.1.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 773310c3ba..5b6d9e669e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.1.0-SNAPSHOT +version=3.1.0 org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true From 0a61ca89c88dfb18a0736a8d757a3365e2966468 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 20 Nov 2023 16:43:05 +0000 Subject: [PATCH 302/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 5b6d9e669e..bf6d0d0664 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.1.0 +version=3.1.1-SNAPSHOT org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true From 44581b524cf0c764ed27b6b4f1d684e07a67534e Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Tue, 21 Nov 2023 15:11:20 -0500 Subject: [PATCH 303/737] Fix What's New Anchor --- src/reference/asciidoc/whats-new.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index ce30779d1c..23218ec70b 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -7,7 +7,7 @@ This version requires Spring Framework 6.1 and Java 17. -[[31-exc]] +[[x31-exc]] ==== Exclusive Consumer Logging Log messages reporting access refusal due to exclusive consumers are now logged at DEBUG level by default. From 6ea4305dfddebd3676920202ef05ff55600afd04 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 28 Nov 2023 11:51:19 -0500 Subject: [PATCH 304/737] Change developer roles and emails --- gradle/publish-maven.gradle | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/gradle/publish-maven.gradle b/gradle/publish-maven.gradle index ff0b457510..c83c8d33fb 100644 --- a/gradle/publish-maven.gradle +++ b/gradle/publish-maven.gradle @@ -27,31 +27,41 @@ publishing { developerConnection = linkScmDevConnection } developers { + developer { + id = 'artembilan' + name = 'Artem Bilan' + email = 'artem.bilan@broadcom.com' + roles = ['project lead'] + } developer { id = 'garyrussell' name = 'Gary Russell' - email = 'grussell@vmware.com' - roles = ['project lead'] + email = 'github@gprussell.net' + roles = ['project lead emeritus'] } developer { - id = 'artembilan' - name = 'Artem Bilan' - email = 'abilan@vmware.com' + id = 'sobychacko' + name = 'Soby Chacko' + email = 'soby.chacko@broadcom.com' + roles = ['contributor'] } developer { - id = 'davesyer' + id = 'dsyer' name = 'Dave Syer' - email = 'dsyer@vmware.com' + email = 'david.syer@broadcom.com' + roles = ['project founder'] } developer { id = 'markfisher' name = 'Mark Fisher' - email = 'markfisher@vmware.com' + email = 'mark.fisher@broadcom.com' + roles = ['project founder'] } developer { id = 'markpollack' name = 'Mark Pollack' - email = 'mpollack@vmware.com' + email = 'mark.pollack@broadcom.com' + roles = ['project founder'] } } issueManagement { From bbeed2e59222ab11d18095304a9d7a9625f5b9f4 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 28 Nov 2023 16:50:00 -0500 Subject: [PATCH 305/737] Migrate CI/CD to GHA * Upgrade to Gradle `8.4` and latest plugins for Develocity (formerly Gradle Enterprise) * Add GHA workflows based on a common repository with reusable workflows * Remove `release-files-spec.json` as it is supplied by common repo on respective CI/CD phase * Remove obsolete GHA workflows * Fix deprecation in the `AbstractTestContainerTests` * Fix `rawtypes` warning in the `SimpleMessageListenerContainerIntegration2Tests` --- .github/release-files-spec.json | 31 ---------- .github/workflows/central-sync-close.yml | 23 ------- .github/workflows/central-sync-create.yml | 58 ------------------ .github/workflows/central-sync-release.yml | 23 ------- .github/workflows/ci-snapshot.yml | 16 +++++ .github/workflows/pr-build-workflow.yml | 38 ------------ .github/workflows/pr-build.yml | 10 +++ .github/workflows/release.yml | 23 +++++++ .github/workflows/verify-staged-artifacts.yml | 47 ++++++++++++++ build.gradle | 44 +------------ gradle/publish-maven.gradle | 6 -- gradle/wrapper/gradle-wrapper.jar | Bin 63375 -> 63721 bytes gradle/wrapper/gradle-wrapper.properties | 5 +- gradlew | 17 ++--- settings.gradle | 4 +- .../junit/AbstractTestContainerTests.java | 14 ++++- ...ageListenerContainerIntegration2Tests.java | 4 +- 17 files changed, 125 insertions(+), 238 deletions(-) delete mode 100644 .github/release-files-spec.json delete mode 100644 .github/workflows/central-sync-close.yml delete mode 100644 .github/workflows/central-sync-create.yml delete mode 100644 .github/workflows/central-sync-release.yml create mode 100644 .github/workflows/ci-snapshot.yml delete mode 100644 .github/workflows/pr-build-workflow.yml create mode 100644 .github/workflows/pr-build.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/verify-staged-artifacts.yml diff --git a/.github/release-files-spec.json b/.github/release-files-spec.json deleted file mode 100644 index ee43685f90..0000000000 --- a/.github/release-files-spec.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "files": [ - { - "aql": { - "items.find": { - "$and": [ - { - "@build.name": "${buildname}", - "@build.number": "${buildnumber}", - "path": {"$match": "org*"} - }, - { - "$or": [ - { - "name": {"$match": "*.pom"} - }, - { - "name": {"$match": "*.jar"} - }, - { - "name": {"$match": "*.module"} - } - ] - } - ] - } - }, - "target": "nexus/" - } - ] -} diff --git a/.github/workflows/central-sync-close.yml b/.github/workflows/central-sync-close.yml deleted file mode 100644 index a00aebd4d4..0000000000 --- a/.github/workflows/central-sync-close.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Central Sync Close - -on: - workflow_dispatch: - inputs: - stagedRepositoryId: - description: "Staged repository id" - required: true - -jobs: - build: - runs-on: ubuntu-latest - steps: - - # Request close promotion repo - - uses: jvalkeal/nexus-sync@v0 - with: - url: ${{ secrets.OSSRH_URL }} - username: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }} - password: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }} - staging-profile-name: ${{ secrets.OSSRH_STAGING_PROFILE_NAME }} - staging-repo-id: ${{ github.event.inputs.stagedRepositoryId }} - close: true diff --git a/.github/workflows/central-sync-create.yml b/.github/workflows/central-sync-create.yml deleted file mode 100644 index 5f7f0e7df2..0000000000 --- a/.github/workflows/central-sync-create.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Central Sync Create - -on: - workflow_dispatch: - inputs: - buildName: - description: "Artifactory build name" - required: true - buildNumber: - description: "Artifactory build number" - required: true - -jobs: - build: - runs-on: ubuntu-latest - steps: - - # to get spec file in .github - - uses: actions/checkout@v2 - - # Setup jfrog cli - - uses: jfrog/setup-jfrog-cli@v1 - with: - version: 1.51.1 - env: - JF_ARTIFACTORY_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} - - # Extract build id from input - - name: Extract Build Id - run: | - echo JFROG_CLI_BUILD_NAME="${{ github.event.inputs.buildName }}" >> $GITHUB_ENV - echo JFROG_CLI_BUILD_NUMBER=${{ github.event.inputs.buildNumber }} >> $GITHUB_ENV - - # Download released files - - name: Download Release Files - run: | - jfrog rt download \ - --spec .github/release-files-spec.json \ - --spec-vars "buildname=$JFROG_CLI_BUILD_NAME;buildnumber=$JFROG_CLI_BUILD_NUMBER" - - # Create checksums, signatures and create staging repo on central and upload - - uses: jvalkeal/nexus-sync@v0 - id: nexus - with: - url: ${{ secrets.OSSRH_URL }} - username: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }} - password: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }} - staging-profile-name: ${{ secrets.OSSRH_STAGING_PROFILE_NAME }} - create: true - upload: true - generate-checksums: true - pgp-sign: true - pgp-sign-passphrase: ${{ secrets.GPG_PASSPHRASE }} - pgp-sign-private-key: ${{ secrets.GPG_PRIVATE_KEY }} - - # Print staging repo id - - name: Print Staging Repo Id - run: echo ${{ steps.nexus.outputs.staged-repository-id }} diff --git a/.github/workflows/central-sync-release.yml b/.github/workflows/central-sync-release.yml deleted file mode 100644 index abf2cb92f2..0000000000 --- a/.github/workflows/central-sync-release.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Central Sync Release - -on: - workflow_dispatch: - inputs: - stagedRepositoryId: - description: "Staged repository id" - required: true - -jobs: - build: - runs-on: ubuntu-latest - steps: - - # Request release promotion repo - - uses: jvalkeal/nexus-sync@v0 - with: - url: ${{ secrets.OSSRH_URL }} - username: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }} - password: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }} - staging-profile-name: ${{ secrets.OSSRH_STAGING_PROFILE_NAME }} - staging-repo-id: ${{ github.event.inputs.stagedRepositoryId }} - release: true diff --git a/.github/workflows/ci-snapshot.yml b/.github/workflows/ci-snapshot.yml new file mode 100644 index 0000000000..92496aacfe --- /dev/null +++ b/.github/workflows/ci-snapshot.yml @@ -0,0 +1,16 @@ +name: CI SNAPSHOT + +on: + workflow_dispatch: + push: + branches: + - main + +jobs: + build-snapshot: + uses: artembilan/spring-github-workflows/.github/workflows/spring-artifactory-gradle-snapshot.yml@main + secrets: + GRADLE_ENTERPRISE_CACHE_USER: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} + GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} + GRADLE_ENTERPRISE_SECRET_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + JF_ARTIFACTORY_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} \ No newline at end of file diff --git a/.github/workflows/pr-build-workflow.yml b/.github/workflows/pr-build-workflow.yml deleted file mode 100644 index 5c1cb562d2..0000000000 --- a/.github/workflows/pr-build-workflow.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Pull Request build - -on: - pull_request: - branches: [ main ] - -jobs: - build: - - runs-on: ubuntu-latest - - services: - rabbitmq: - image: rabbitmq:management - ports: - - 5672:5672 - - 15762:15762 - - steps: - - uses: actions/checkout@v2 - - - name: Set up JDK 17 - uses: actions/setup-java@v1 - with: - java-version: 17 - - - name: Run Gradle - uses: burrunan/gradle-cache-action@v1 - with: - arguments: check - - - name: Capture Test Results - if: failure() - uses: actions/upload-artifact@v2 - with: - name: test-results - path: '*/build/reports/tests/**/*.*' - retention-days: 3 diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml new file mode 100644 index 0000000000..246f6a7f11 --- /dev/null +++ b/.github/workflows/pr-build.yml @@ -0,0 +1,10 @@ +name: Pull Request Build + +on: + pull_request: + branches: + - main + +jobs: + build-pull-request: + uses: artembilan/spring-github-workflows/.github/workflows/spring-gradle-pull-request-build.yml@main diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..24dd09d29c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,23 @@ +name: Release + +on: + workflow_dispatch: + +run-name: Release current version for branch ${{ github.ref_name }} + +jobs: + release: + uses: artembilan/spring-github-workflows/.github/workflows/spring-artifactory-release.yml@main + secrets: + GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} + GRADLE_ENTERPRISE_CACHE_USER: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} + GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} + GRADLE_ENTERPRISE_SECRET_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + JF_ARTIFACTORY_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} + OSSRH_URL: ${{ secrets.OSSRH_URL }} + OSSRH_S01_TOKEN_USERNAME: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }} + OSSRH_S01_TOKEN_PASSWORD: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }} + OSSRH_STAGING_PROFILE_NAME: ${{ secrets.OSSRH_STAGING_PROFILE_NAME }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + SPRING_RELEASE_SLACK_WEBHOOK_URL: ${{ secrets.SPRING_RELEASE_SLACK_WEBHOOK_URL }} \ No newline at end of file diff --git a/.github/workflows/verify-staged-artifacts.yml b/.github/workflows/verify-staged-artifacts.yml new file mode 100644 index 0000000000..2ce9e9285b --- /dev/null +++ b/.github/workflows/verify-staged-artifacts.yml @@ -0,0 +1,47 @@ +name: Verify Staged Artifacts + +on: + workflow_dispatch: + inputs: + releaseVersion: + description: 'Release version like 5.0.0-M1, 5.1.0-RC1, 5.2.0 etc.' + required: true + type: string + +jobs: + verify-staged-with-samples: + runs-on: ubuntu-latest + steps: + + - name: Checkout Samples Repo + uses: actions/checkout@v4 + with: + repository: spring-projects/spring-amqp-samples + show-progress: false + + - name: Set up JDK + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 17 + cache: 'maven' + + - uses: jfrog/setup-jfrog-cli@v3 + - name: Configure JFrog Cli + run: | + jf mvnc \ + --repo-resolve-releases=libs-staging-local \ + --repo-resolve-snapshots=snapshot \ + --repo-deploy-releases=libs-milestone-local \ + --repo-deploy-snapshots=libs-snapshot-local + + - name: Verify samples against staged release + run: jf mvn verify -D"spring.amqp.version"=${{ inputs.releaseVersion }} + + - name: Capture Test Results + if: failure() + uses: actions/upload-artifact@v3 + with: + name: test-results + path: '**/target/surefire-reports/**/*.*' + retention-days: 1 \ No newline at end of file diff --git a/build.gradle b/build.gradle index 499c81ccbc..086037186d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext.kotlinVersion = '1.9.10' - ext.isCI = System.getenv('GITHUB_ACTION') || System.getenv('bamboo_buildKey') + ext.isCI = System.getenv('GITHUB_ACTION') repositories { mavenCentral() gradlePluginPortal() @@ -19,11 +19,9 @@ plugins { id 'base' id 'project-report' id 'idea' - id 'org.sonarqube' version '4.3.0.3225' - id 'org.ajoberstar.grgit' version '4.0.1' + id 'org.ajoberstar.grgit' version '4.1.1' id 'io.spring.nohttp' version '0.0.11' - id 'io.spring.dependency-management' version '1.0.9.RELEASE' apply false - id 'com.jfrog.artifactory' version '4.33.1' apply false + id 'io.spring.dependency-management' version '1.1.4' apply false id 'org.asciidoctor.jvm.pdf' version '3.3.2' id 'org.asciidoctor.jvm.convert' version '3.3.2' } @@ -135,7 +133,6 @@ configure(javaProjects) { subproject -> apply plugin: 'eclipse' apply plugin: 'idea' apply plugin: 'project-report' - apply plugin: 'jacoco' apply plugin: 'checkstyle' apply plugin: 'kotlin' apply plugin: 'kotlin-spring' @@ -168,10 +165,6 @@ configure(javaProjects) { subproject -> } } - jacoco { - toolVersion = '0.8.10' - } - // dependencies that are common across all java projects dependencies { compileOnly "com.google.code.findbugs:jsr305:$googleJsr305Version" @@ -279,25 +272,8 @@ configure(javaProjects) { subproject -> showStackTraces = true exceptionFormat = 'full' } - - jacoco { - destinationFile = file("$buildDir/jacoco.exec") - } - - } - - jacocoTestReport { - onlyIf { System.properties['sonar.host.url'] } - dependsOn test - reports { - xml.required = true - csv.required = false - html.required = false - } } - rootProject.tasks['sonarqube'].dependsOn jacocoTestReport - task testAll(type: Test, dependsOn: check) gradle.taskGraph.whenReady { graph -> @@ -402,10 +378,6 @@ project('spring-amqp-bom') { } } } - - sonarqube { - skipProject = true - } } project('spring-rabbit') { @@ -644,16 +616,6 @@ task reference(dependsOn: asciidoctor) { description = 'Generate the reference documentation' } -sonarqube { - properties { - property 'sonar.links.homepage', linkHomepage - property 'sonar.links.ci', linkCi - property 'sonar.links.issue', linkIssue - property 'sonar.links.scm', linkScmUrl - property 'sonar.links.scm_dev', linkScmDevConnection - } -} - task api(type: Javadoc) { group = 'Documentation' description = 'Generates aggregated Javadoc API documentation.' diff --git a/gradle/publish-maven.gradle b/gradle/publish-maven.gradle index c83c8d33fb..0b72dba7c3 100644 --- a/gradle/publish-maven.gradle +++ b/gradle/publish-maven.gradle @@ -1,5 +1,4 @@ apply plugin: 'maven-publish' -apply plugin: 'com.jfrog.artifactory' publishing { publications { @@ -80,8 +79,3 @@ publishing { } } } - -artifactoryPublish { - dependsOn build - publications(publishing.publications.mavenJava) -} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 033e24c4cdf41af1ab109bc7f253b2b887023340..7f93135c49b765f8051ef9d0a6055ff8e46073d8 100644 GIT binary patch delta 28216 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 19d44294b5..46671acb6e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,9 +1,8 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=03ec176d388f2aa99defcadc3ac6adf8dd2bce5145a129659537c0874dea5ad1 -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +distributionSha256Sum=3e1af3ae886920c3ac87f7a91f816c0c7c436f276a6eefdb3da152100fef72ae +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists - diff --git a/gradlew b/gradlew index fcb6fca147..1aa94a4269 100755 --- a/gradlew +++ b/gradlew @@ -83,7 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -201,11 +202,11 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/settings.gradle b/settings.gradle index ccd2abcf6d..d2eed3f267 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,6 @@ plugins { - id 'com.gradle.enterprise' version '3.13.4' - id 'io.spring.ge.conventions' version '0.0.13' + id 'com.gradle.enterprise' version '3.14.1' + id 'io.spring.ge.conventions' version '0.0.14' } rootProject.name = 'spring-amqp-dist' diff --git a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/AbstractTestContainerTests.java b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/AbstractTestContainerTests.java index f3246d9ab5..0d45fc7aec 100644 --- a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/AbstractTestContainerTests.java +++ b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/AbstractTestContainerTests.java @@ -16,14 +16,18 @@ package org.springframework.amqp.rabbit.junit; +import java.io.IOException; import java.time.Duration; +import org.junit.jupiter.api.BeforeAll; import org.testcontainers.containers.RabbitMQContainer; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; /** * @author Gary Russell + * @author Artem Bilan + * * @since 2.4 * */ @@ -35,7 +39,7 @@ public abstract class AbstractTestContainerTests { static { if (System.getProperty("spring.rabbit.use.local.server") == null && System.getenv("SPRING_RABBIT_USE_LOCAL_SERVER") == null) { - String image = "rabbitmq:3.11-management"; + String image = "rabbitmq:management"; String cache = System.getenv().get("IMAGE_CACHE"); if (cache != null) { image = cache + image; @@ -44,15 +48,19 @@ public abstract class AbstractTestContainerTests { .asCompatibleSubstituteFor("rabbitmq"); RABBITMQ = new RabbitMQContainer(imageName) .withExposedPorts(5672, 15672, 5552) - .withPluginsEnabled("rabbitmq_stream") .withStartupTimeout(Duration.ofMinutes(2)); - RABBITMQ.start(); } else { RABBITMQ = null; } } + @BeforeAll + static void startContainer() throws IOException, InterruptedException { + RABBITMQ.start(); + RABBITMQ.execInContainer("rabbitmq-plugins", "enable", "rabbitmq_stream"); + } + public static int amqpPort() { return RABBITMQ != null ? RABBITMQ.getAmqpPort() : 5672; } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java index 290b7d759d..ec13130380 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java @@ -103,7 +103,7 @@ public class SimpleMessageListenerContainerIntegration2Tests { public static final String TEST_QUEUE_1 = "test.queue.1.SimpleMessageListenerContainerIntegration2Tests"; - private static Log logger = LogFactory.getLog(SimpleMessageListenerContainerIntegration2Tests.class); + private static final Log logger = LogFactory.getLog(SimpleMessageListenerContainerIntegration2Tests.class); private final ExecutorService executorService = Executors.newSingleThreadExecutor(); @@ -415,7 +415,7 @@ else if (event instanceof ConsumeOkEvent) { ArgumentCaptor contLogCaptor = ArgumentCaptor.forClass(String.class); verify(containerLogger, atLeastOnce()).debug(contLogCaptor.capture()); assertThat(contLogCaptor.getAllValues()).anyMatch(arg -> arg.contains("exclusive")); - ArgumentCaptor lmCaptor = ArgumentCaptor.forClass(LogMessage.class); + ArgumentCaptor lmCaptor = ArgumentCaptor.forClass(LogMessage.class); verify(containerLogger).debug(lmCaptor.capture()); assertThat(lmCaptor.getAllValues()).anyMatch(arg -> arg.toString().startsWith("Restarting ")); } From 8eabfaae298cc4661ab7dce3aa829665df645fe4 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 28 Nov 2023 17:21:56 -0500 Subject: [PATCH 306/737] Change build badge in README to GHA --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8da434b771..70ef04aecd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Spring AMQP [](https://build.spring.io/browse/AAMQP-MAIN) +Spring AMQP ![Build Status](https://github.com/spring-projects/spring-amqp/actions/workflows/ci-snapshot.yml/badge.svg) [![Revved up by Gradle Enterprise](https://img.shields.io/badge/Revved%20up%20by-Gradle%20Enterprise-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.spring.io/scans?search.rootProjectNames=spring-amqp) =========== From d8fbc1a163b1c1fd3ea5e53770c704d56500fdda Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 29 Nov 2023 13:27:20 -0500 Subject: [PATCH 307/737] Add link to CI workflow for badge in the README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 70ef04aecd..1cd970efa6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Spring AMQP ![Build Status](https://github.com/spring-projects/spring-amqp/actions/workflows/ci-snapshot.yml/badge.svg) +Spring AMQP [![Build Status](https://github.com/spring-projects/spring-amqp/actions/workflows/ci-snapshot.yml/badge.svg)](https://github.com/spring-projects/spring-amqp/actions/workflows/ci-snapshot.yml) [![Revved up by Gradle Enterprise](https://img.shields.io/badge/Revved%20up%20by-Gradle%20Enterprise-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.spring.io/scans?search.rootProjectNames=spring-amqp) =========== From 5baad07dc300ff849f84a263ac64c952e1b4b3d3 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 29 Nov 2023 13:33:24 -0500 Subject: [PATCH 308/737] Fix verify-staged-artifacts.yml for JFrog * Attempt to configure JFrog only for staging repo --- .github/workflows/verify-staged-artifacts.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/verify-staged-artifacts.yml b/.github/workflows/verify-staged-artifacts.yml index 2ce9e9285b..5e3ff12b2d 100644 --- a/.github/workflows/verify-staged-artifacts.yml +++ b/.github/workflows/verify-staged-artifacts.yml @@ -27,13 +27,11 @@ jobs: cache: 'maven' - uses: jfrog/setup-jfrog-cli@v3 + env: + JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} + - name: Configure JFrog Cli - run: | - jf mvnc \ - --repo-resolve-releases=libs-staging-local \ - --repo-resolve-snapshots=snapshot \ - --repo-deploy-releases=libs-milestone-local \ - --repo-deploy-snapshots=libs-snapshot-local + run: jf mvnc --repo-resolve-releases=libs-staging-local - name: Verify samples against staged release run: jf mvn verify -D"spring.amqp.version"=${{ inputs.releaseVersion }} From 18f21c9ed12e71287363f36dde0a9cd505517cc2 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 29 Nov 2023 13:37:22 -0500 Subject: [PATCH 309/737] Add `--repo-resolve-snapshots` for JFrog --- .github/workflows/verify-staged-artifacts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verify-staged-artifacts.yml b/.github/workflows/verify-staged-artifacts.yml index 5e3ff12b2d..c8e2e940ed 100644 --- a/.github/workflows/verify-staged-artifacts.yml +++ b/.github/workflows/verify-staged-artifacts.yml @@ -31,7 +31,7 @@ jobs: JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} - name: Configure JFrog Cli - run: jf mvnc --repo-resolve-releases=libs-staging-local + run: jf mvnc --repo-resolve-releases=libs-staging-local --repo-resolve-snapshots=snapshot - name: Verify samples against staged release run: jf mvn verify -D"spring.amqp.version"=${{ inputs.releaseVersion }} From 9ccbbad7f33def3b8aa0bb7bdd47026f5d25fd07 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 29 Nov 2023 13:52:57 -0500 Subject: [PATCH 310/737] Rework `mvnc` for other repos --- .github/workflows/verify-staged-artifacts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verify-staged-artifacts.yml b/.github/workflows/verify-staged-artifacts.yml index c8e2e940ed..78c8a7f63b 100644 --- a/.github/workflows/verify-staged-artifacts.yml +++ b/.github/workflows/verify-staged-artifacts.yml @@ -31,7 +31,7 @@ jobs: JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} - name: Configure JFrog Cli - run: jf mvnc --repo-resolve-releases=libs-staging-local --repo-resolve-snapshots=snapshot + run: jf mvnc --repo-resolve-releases=release --repo-resolve-snapshots=libs-staging-local - name: Verify samples against staged release run: jf mvn verify -D"spring.amqp.version"=${{ inputs.releaseVersion }} From 41e500ecc4db85a3d797e21a96552fd921aadd0b Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 29 Nov 2023 13:59:38 -0500 Subject: [PATCH 311/737] Try to use `libs-release` * Add `-B -ntp` option to `mvn` command --- .github/workflows/verify-staged-artifacts.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/verify-staged-artifacts.yml b/.github/workflows/verify-staged-artifacts.yml index 78c8a7f63b..2aa8300530 100644 --- a/.github/workflows/verify-staged-artifacts.yml +++ b/.github/workflows/verify-staged-artifacts.yml @@ -31,10 +31,10 @@ jobs: JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} - name: Configure JFrog Cli - run: jf mvnc --repo-resolve-releases=release --repo-resolve-snapshots=libs-staging-local + run: jf mvnc --repo-resolve-releases=libs-release --repo-resolve-snapshots=libs-staging-local - name: Verify samples against staged release - run: jf mvn verify -D"spring.amqp.version"=${{ inputs.releaseVersion }} + run: jf mvn verify -D"spring.amqp.version"=${{ inputs.releaseVersion }} -B -ntp - name: Capture Test Results if: failure() From 02471a990811c9aa30d4d8d1d2f85866c6bc4415 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 29 Nov 2023 14:09:14 -0500 Subject: [PATCH 312/737] Try with multi repos --- .github/workflows/verify-staged-artifacts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verify-staged-artifacts.yml b/.github/workflows/verify-staged-artifacts.yml index 2aa8300530..278486aac2 100644 --- a/.github/workflows/verify-staged-artifacts.yml +++ b/.github/workflows/verify-staged-artifacts.yml @@ -31,7 +31,7 @@ jobs: JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} - name: Configure JFrog Cli - run: jf mvnc --repo-resolve-releases=libs-release --repo-resolve-snapshots=libs-staging-local + run: jf mvnc --repo-resolve-releases=libs-release,libs-staging-local --repo-resolve-snapshots=snapshot - name: Verify samples against staged release run: jf mvn verify -D"spring.amqp.version"=${{ inputs.releaseVersion }} -B -ntp From 6cad2fca301a7ab6ab5017a1a15dc47bab9fa48f Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 29 Nov 2023 14:36:28 -0500 Subject: [PATCH 313/737] Use virtual repo for staging --- .github/workflows/verify-staged-artifacts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verify-staged-artifacts.yml b/.github/workflows/verify-staged-artifacts.yml index 278486aac2..77cccbac8d 100644 --- a/.github/workflows/verify-staged-artifacts.yml +++ b/.github/workflows/verify-staged-artifacts.yml @@ -31,7 +31,7 @@ jobs: JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} - name: Configure JFrog Cli - run: jf mvnc --repo-resolve-releases=libs-release,libs-staging-local --repo-resolve-snapshots=snapshot + run: jf mvnc --repo-resolve-releases=libs-spring-dataflow-private-staging-release --repo-resolve-snapshots=snapshot - name: Verify samples against staged release run: jf mvn verify -D"spring.amqp.version"=${{ inputs.releaseVersion }} -B -ntp From efc3637e077b71f28aeacdf74ff337a1c545b70c Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 30 Nov 2023 18:05:05 -0500 Subject: [PATCH 314/737] Migrate to Gradle-specific reusable workflow * Add `permissions` for `GH_TOKEN` in GH Actions --- .github/workflows/release.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 24dd09d29c..f2c3c9b3d0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,7 +7,10 @@ run-name: Release current version for branch ${{ github.ref_name }} jobs: release: - uses: artembilan/spring-github-workflows/.github/workflows/spring-artifactory-release.yml@main + permissions: + actions: write + + uses: artembilan/spring-github-workflows/.github/workflows/spring-artifactory-gradle-release.yml@main secrets: GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} GRADLE_ENTERPRISE_CACHE_USER: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} From bf2a391a0c1781d4d3c354be21cf0676f53c3acd Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 1 Dec 2023 09:43:42 -0500 Subject: [PATCH 315/737] Fix typos in the `SerializationUtils` --- .../org/springframework/amqp/utils/SerializationUtils.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/utils/SerializationUtils.java b/spring-amqp/src/main/java/org/springframework/amqp/utils/SerializationUtils.java index 89690df450..6e9cb8de8c 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/utils/SerializationUtils.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/utils/SerializationUtils.java @@ -151,8 +151,7 @@ public static void checkAllowedList(Class clazz, Set patterns) { if (TRUST_ALL && ObjectUtils.isEmpty(patterns)) { return; } - if (clazz.isArray() || clazz.isPrimitive() || clazz.equals(String.class) - || Number.class.isAssignableFrom(clazz) + if (clazz.isArray() || clazz.isPrimitive() || Number.class.isAssignableFrom(clazz) || String.class.equals(clazz)) { return; } @@ -163,7 +162,7 @@ public static void checkAllowedList(Class clazz, Set patterns) { } } throw new SecurityException("Attempt to deserialize unauthorized " + clazz - + "; add allowed class name patterns to the message converter or, if you trust the message orginiator, " + + "; add allowed class name patterns to the message converter or, if you trust the message originator, " + "set environment variable '" + TRUST_ALL_ENV + "' or system property '" + TRUST_ALL_PROP + "' to true"); } From 9cc8e4996a51a0496cc47ea6f8165703696995c7 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 5 Dec 2023 14:33:11 -0600 Subject: [PATCH 316/737] Migrate docs to Antora infrastructure * Migrate Structure * Insert explicit ids for headers * Remove unnecessary asciidoc attributes * Fix image::image * Copy default antora files * Fix indentation for all pages * Split files * Generate a default navigation * Remove includes * Fix cross references * Enable Section Summary TOC for small pages * Remove src/reference/asciidoc * Fix antora build files * Fix Antora Build and Migrate to Asciidoctor Tabs * Fix Antora Nav Hiearchy * Add deploy-docs.yml * PR Feedback * Fix `modifiedFiles` for `updateCopyrights` task --- .github/workflows/deploy-docs.yml | 32 + build.gradle | 40 +- src/reference/antora/antora-playbook.yml | 44 + src/reference/antora/antora.yml | 17 + .../ROOT/assets}/images/cacheStats.png | Bin .../modules/ROOT/assets}/images/tickmark.png | Bin src/reference/antora/modules/ROOT/nav.adoc | 88 + .../antora/modules/ROOT/pages/amqp.adoc | 6 + .../modules/ROOT/pages/amqp/abstractions.adoc | 183 + .../ROOT/pages/amqp/broker-configuration.adoc | 682 ++ .../ROOT/pages/amqp/broker-events.adoc | 26 + .../modules/ROOT/pages/amqp/connections.adoc | 839 ++ .../ROOT/pages/amqp/containerAttributes.adoc | 734 ++ .../containers-and-broker-named-queues.adoc | 35 + .../ROOT/pages/amqp/custom-client-props.adoc | 15 + .../modules/ROOT/pages/amqp/debugging.adoc | 12 + .../pages/amqp/delayed-message-exchange.adoc | 51 + .../ROOT/pages/amqp/exception-handling.adoc | 46 + .../ROOT/pages/amqp/exclusive-consumer.adoc | 10 + .../ROOT/pages/amqp/listener-concurrency.adoc | 46 + .../ROOT/pages/amqp/listener-queues.adoc | 19 + .../ROOT/pages/amqp/management-rest-api.adoc | 14 + .../ROOT/pages/amqp/message-converters.adoc | 513 ++ .../modules/ROOT/pages/amqp/multi-rabbit.adoc | 149 + .../ROOT/pages/amqp/post-processing.adoc | 35 + .../ROOT/pages/amqp/receiving-messages.adoc | 10 + .../async-annotation-driven.adoc | 124 + .../container-management.adoc | 23 + .../async-annotation-driven/conversion.adoc | 101 + .../custom-argument-resolver.adoc | 36 + .../enable-signature.adoc | 71 + .../async-annotation-driven/enable.adoc | 121 + .../error-handling.adoc | 55 + .../async-annotation-driven/meta.adoc | 71 + .../method-selection.adoc | 47 + .../multiple-queues.adoc | 40 + .../proxy-rabbitlistener-and-generics.adoc | 46 + .../rabbit-validation.adoc | 69 + .../async-annotation-driven/registration.adoc | 29 + .../repeatable-rabbit-listener.adoc | 10 + .../reply-content-type.adoc | 51 + .../async-annotation-driven/reply.adoc | 158 + .../receiving-messages/async-consumer.adoc | 275 + .../receiving-messages/async-returns.adoc | 24 + .../pages/amqp/receiving-messages/batch.adoc | 90 + .../receiving-messages/choose-container.adoc | 36 + .../receiving-messages/consumer-events.adoc | 39 + .../amqp/receiving-messages/consumerTags.adoc | 21 + .../amqp/receiving-messages/de-batching.adoc | 16 + .../receiving-messages/idle-containers.adoc | 95 + .../micrometer-observation.adoc | 19 + .../amqp/receiving-messages/micrometer.adoc | 21 + .../receiving-messages/polling-consumer.adoc | 102 + .../amqp/receiving-messages/threading.adoc | 28 + .../using-container-factories.adoc | 51 + .../ROOT/pages/amqp/request-reply.adoc | 301 + ...ering-from-errors-and-broker-failures.adoc | 212 + .../ROOT/pages/amqp/sending-messages.adoc | 224 + .../modules/ROOT/pages/amqp/template.adoc | 547 ++ .../modules/ROOT/pages/amqp/transactions.adoc | 157 + .../ROOT/pages/appendix/change-history.adoc | 5 + .../ROOT/pages/appendix/current-release.adoc | 6 + .../ROOT/pages/appendix/micrometer.adoc | 8 + .../modules/ROOT/pages/appendix/native.adoc | 7 + .../pages/appendix/previous-whats-new.adoc | 4 + .../changes-in-1-3-since-1-2.adoc | 107 + .../changes-in-1-4-since-1-3.adoc | 120 + .../changes-in-1-5-since-1-4.adoc | 197 + .../changes-in-1-6-since-1-5.adoc | 262 + .../changes-in-1-7-since-1-6.adoc | 76 + .../changes-in-2-0-since-1-7.adoc | 203 + .../changes-in-2-1-since-2-0.adoc | 127 + .../changes-in-2-2-since-2-1.adoc | 137 + .../changes-in-2-3-since-2-2.adoc | 72 + .../changes-in-2-4-since-2-3.adoc | 24 + .../changes-in-3-0-since-2-4.adoc | 70 + .../changes-to-1-1-since-1-0.adoc | 21 + .../changes-to-1-2-since-1-1.adoc | 58 + .../previous-whats-new/earlier-releases.adoc | 6 + .../message-converter-changes-1.adoc | 7 + .../message-converter-changes.adoc | 7 + .../stream-support-changes.adoc | 7 + .../modules/ROOT/pages}/further-reading.adoc | 3 +- .../modules/ROOT/pages/index.adoc} | 14 + .../ROOT/pages/integration-reference.adoc | 4 + .../ROOT/pages/introduction/index.adoc | 5 + .../ROOT/pages/introduction}/quick-tour.adoc | 33 +- .../modules/ROOT/pages}/logging.adoc | 50 +- .../antora/modules/ROOT/pages/reference.adoc | 9 + .../antora/modules/ROOT/pages/resources.adoc | 6 + .../modules/ROOT/pages}/sample-apps.adoc | 49 +- .../modules/ROOT/pages}/si-amqp.adoc | 24 +- .../modules/ROOT/pages}/stream.adoc | 61 +- .../modules/ROOT/pages}/testing.adoc | 70 +- .../modules/ROOT/pages}/whats-new.adoc | 13 +- src/reference/asciidoc/amqp.adoc | 7059 ----------------- src/reference/asciidoc/appendix.adoc | 1329 ---- src/reference/asciidoc/docinfo.html | 5 - src/reference/asciidoc/index.adoc | 70 - 99 files changed, 8627 insertions(+), 8664 deletions(-) create mode 100644 .github/workflows/deploy-docs.yml create mode 100644 src/reference/antora/antora-playbook.yml create mode 100644 src/reference/antora/antora.yml rename src/reference/{asciidoc => antora/modules/ROOT/assets}/images/cacheStats.png (100%) rename src/reference/{asciidoc => antora/modules/ROOT/assets}/images/tickmark.png (100%) create mode 100644 src/reference/antora/modules/ROOT/nav.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/abstractions.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/broker-events.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/connections.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/containerAttributes.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/containers-and-broker-named-queues.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/custom-client-props.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/debugging.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/delayed-message-exchange.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/exception-handling.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/exclusive-consumer.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/listener-concurrency.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/listener-queues.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/management-rest-api.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/message-converters.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/multi-rabbit.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/post-processing.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/container-management.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/conversion.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/custom-argument-resolver.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/enable-signature.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/enable.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/error-handling.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/meta.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/method-selection.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/multiple-queues.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/proxy-rabbitlistener-and-generics.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/rabbit-validation.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/registration.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/repeatable-rabbit-listener.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/reply-content-type.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/reply.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-consumer.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-returns.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/batch.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/choose-container.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/consumer-events.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/consumerTags.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/de-batching.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/idle-containers.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer-observation.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/polling-consumer.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/threading.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/using-container-factories.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/request-reply.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/resilience-recovering-from-errors-and-broker-failures.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/sending-messages.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/template.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/amqp/transactions.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/appendix/change-history.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/appendix/current-release.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/appendix/micrometer.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/appendix/native.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-3-since-1-2.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-4-since-1-3.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-5-since-1-4.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-6-since-1-5.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-7-since-1-6.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-0-since-1-7.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-1-since-2-0.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-2-since-2-1.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-3-since-2-2.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-4-since-2-3.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-3-0-since-2-4.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-to-1-1-since-1-0.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-to-1-2-since-1-1.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/earlier-releases.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/message-converter-changes-1.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/message-converter-changes.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/stream-support-changes.adoc rename src/reference/{asciidoc => antora/modules/ROOT/pages}/further-reading.adoc (94%) rename src/reference/{asciidoc/preface.adoc => antora/modules/ROOT/pages/index.adoc} (51%) create mode 100644 src/reference/antora/modules/ROOT/pages/integration-reference.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/introduction/index.adoc rename src/reference/{asciidoc => antora/modules/ROOT/pages/introduction}/quick-tour.adoc (93%) rename src/reference/{asciidoc => antora/modules/ROOT/pages}/logging.adoc (93%) create mode 100644 src/reference/antora/modules/ROOT/pages/reference.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/resources.adoc rename src/reference/{asciidoc => antora/modules/ROOT/pages}/sample-apps.adoc (96%) rename src/reference/{asciidoc => antora/modules/ROOT/pages}/si-amqp.adoc (92%) rename src/reference/{asciidoc => antora/modules/ROOT/pages}/stream.adoc (92%) rename src/reference/{asciidoc => antora/modules/ROOT/pages}/testing.adoc (94%) rename src/reference/{asciidoc => antora/modules/ROOT/pages}/whats-new.adoc (67%) delete mode 100644 src/reference/asciidoc/amqp.adoc delete mode 100644 src/reference/asciidoc/appendix.adoc delete mode 100644 src/reference/asciidoc/docinfo.html delete mode 100644 src/reference/asciidoc/index.adoc diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 0000000000..f3e899c4fe --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,32 @@ +name: Deploy Docs +on: + push: + branches-ignore: [ gh-pages ] + tags: '**' + repository_dispatch: + types: request-build-reference # legacy + schedule: + - cron: '0 10 * * *' # Once per day at 10am UTC + workflow_dispatch: +permissions: + actions: write +jobs: + build: + runs-on: ubuntu-latest + if: github.repository_owner == 'spring-projects' + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + ref: docs-build + fetch-depth: 1 + - name: Dispatch (partial build) + if: github.ref_type == 'branch' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD) -f build-refname=${{ github.ref_name }} + - name: Dispatch (full build) + if: github.ref_type == 'tag' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD) diff --git a/build.gradle b/build.gradle index 086037186d..ce2a02b674 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,8 @@ plugins { id 'io.spring.dependency-management' version '1.1.4' apply false id 'org.asciidoctor.jvm.pdf' version '3.3.2' id 'org.asciidoctor.jvm.convert' version '3.3.2' + id 'org.antora' version '1.0.0' + id 'io.spring.antora.generate-antora-yml' version '0.0.1' } description = 'Spring AMQP' @@ -38,7 +40,11 @@ ext { springAsciidoctorBackendsVersion = '0.0.7' modifiedFiles = - files(grgit.status().unstaged.modified).filter{ f -> f.name.endsWith('.java') || f.name.endsWith('.kt') } + files() + .from { + files(grgit.status().unstaged.modified) + .filter { f -> f.name.endsWith('.java') || f.name.endsWith('.kt') } + } assertjVersion = '3.24.2' assertkVersion = '0.27.0' @@ -74,6 +80,38 @@ ext { javaProjects = subprojects - project(':spring-amqp-bom') } + + +antora { + version = '3.2.0-alpha.2' + playbook = file('src/reference/antora/antora-playbook.yml') + options = ['to-dir' : project.layout.buildDirectory.dir('site').get().toString(), clean: true, fetch: !project.gradle.startParameter.offline, stacktrace: true] + dependencies = [ + '@antora/atlas-extension': '1.0.0-alpha.1', + '@antora/collector-extension': '1.0.0-alpha.3', + '@asciidoctor/tabs': '1.0.0-beta.3', + '@springio/antora-extensions': '1.4.2', + '@springio/asciidoctor-extensions': '1.0.0-alpha.8', + ] +} + +tasks.named("generateAntoraYml") { + asciidocAttributes = project.provider( { + return ['project-version' : project.version ] + } ) + baseAntoraYmlFile = file('src/reference/antora/antora.yml') +} + +tasks.create(name: 'createAntoraPartials', type: Sync) { + from { tasks.filterMetricsDocsContent.outputs } + into layout.buildDirectory.dir('generated-antora-resources/modules/ROOT/partials') +} + +tasks.create('generateAntoraResources') { + dependsOn 'createAntoraPartials' + dependsOn 'generateAntoraYml' +} + nohttp { source.include '**/src/**' source.exclude '**/*.gif', '**/*.ks' diff --git a/src/reference/antora/antora-playbook.yml b/src/reference/antora/antora-playbook.yml new file mode 100644 index 0000000000..ca962c0696 --- /dev/null +++ b/src/reference/antora/antora-playbook.yml @@ -0,0 +1,44 @@ +antora: + extensions: + - '@springio/antora-extensions/partial-build-extension' + - require: '@springio/antora-extensions/latest-version-extension' + - require: '@springio/antora-extensions/inject-collector-cache-config-extension' + - '@antora/collector-extension' + - '@antora/atlas-extension' + - require: '@springio/antora-extensions/root-component-extension' + root_component_name: 'amqp' + # FIXME: Run antora once using this extension to migrate to the Asciidoc Tabs syntax + # and then remove this extension + - require: '@springio/antora-extensions/tabs-migration-extension' + unwrap_example_block: always + save_result: true +site: + title: Spring AMQP + url: https://docs.spring.io/spring-amqp/reference/ +content: + sources: + - url: ./../../.. + branches: HEAD + # See https://docs.antora.org/antora/latest/playbook/content-source-start-path/#start-path-key + start_path: src/reference/antora + worktrees: true +asciidoc: + attributes: + page-stackoverflow-url: https://stackoverflow.com/tags/spring-amqp + page-pagination: '' + hide-uri-scheme: '@' + tabs-sync-option: '@' + chomp: 'all' + extensions: + - '@asciidoctor/tabs' + - '@springio/asciidoctor-extensions' + sourcemap: true +urls: + latest_version_segment: '' +runtime: + log: + failure_level: warn + format: pretty +ui: + bundle: + url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.9/ui-bundle.zip diff --git a/src/reference/antora/antora.yml b/src/reference/antora/antora.yml new file mode 100644 index 0000000000..6529e29d62 --- /dev/null +++ b/src/reference/antora/antora.yml @@ -0,0 +1,17 @@ +name: amqp +version: true +title: Spring AMQP +nav: + - modules/ROOT/nav.adoc +ext: + collector: + run: + command: gradlew -q "-Dorg.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError" :generateAntoraResources + local: true + scan: + dir: build/generated-antora-resources + +asciidoc: + attributes: + attribute-missing: 'warn' + chomp: 'all' \ No newline at end of file diff --git a/src/reference/asciidoc/images/cacheStats.png b/src/reference/antora/modules/ROOT/assets/images/cacheStats.png similarity index 100% rename from src/reference/asciidoc/images/cacheStats.png rename to src/reference/antora/modules/ROOT/assets/images/cacheStats.png diff --git a/src/reference/asciidoc/images/tickmark.png b/src/reference/antora/modules/ROOT/assets/images/tickmark.png similarity index 100% rename from src/reference/asciidoc/images/tickmark.png rename to src/reference/antora/modules/ROOT/assets/images/tickmark.png diff --git a/src/reference/antora/modules/ROOT/nav.adoc b/src/reference/antora/modules/ROOT/nav.adoc new file mode 100644 index 0000000000..9d71ec1ef1 --- /dev/null +++ b/src/reference/antora/modules/ROOT/nav.adoc @@ -0,0 +1,88 @@ +* xref:index.adoc[] +* xref:whats-new.adoc[] +* xref:introduction/index.adoc[] +** xref:introduction/quick-tour.adoc[] +* xref:reference.adoc[] +** xref:amqp.adoc[] +*** xref:amqp/abstractions.adoc[] +*** xref:amqp/connections.adoc[] +*** xref:amqp/custom-client-props.adoc[] +*** xref:amqp/template.adoc[] +*** xref:amqp/sending-messages.adoc[] +*** xref:amqp/receiving-messages.adoc[] +**** xref:amqp/receiving-messages/polling-consumer.adoc[] +**** xref:amqp/receiving-messages/async-consumer.adoc[] +**** xref:amqp/receiving-messages/de-batching.adoc[] +**** xref:amqp/receiving-messages/consumer-events.adoc[] +**** xref:amqp/receiving-messages/consumerTags.adoc[] +**** xref:amqp/receiving-messages/async-annotation-driven.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/meta.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/enable.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/conversion.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/custom-argument-resolver.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/registration.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/enable-signature.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/rabbit-validation.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/multiple-queues.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/reply.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/reply-content-type.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/method-selection.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/repeatable-rabbit-listener.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/proxy-rabbitlistener-and-generics.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/error-handling.adoc[] +***** xref:amqp/receiving-messages/async-annotation-driven/container-management.adoc[] +**** xref:amqp/receiving-messages/batch.adoc[] +**** xref:amqp/receiving-messages/using-container-factories.adoc[] +**** xref:amqp/receiving-messages/async-returns.adoc[] +**** xref:amqp/receiving-messages/threading.adoc[] +**** xref:amqp/receiving-messages/choose-container.adoc[] +**** xref:amqp/receiving-messages/idle-containers.adoc[] +**** xref:amqp/receiving-messages/micrometer.adoc[] +**** xref:amqp/receiving-messages/micrometer-observation.adoc[] +*** xref:amqp/containers-and-broker-named-queues.adoc[] +*** xref:amqp/message-converters.adoc[] +*** xref:amqp/post-processing.adoc[] +*** xref:amqp/request-reply.adoc[] +*** xref:amqp/broker-configuration.adoc[] +*** xref:amqp/broker-events.adoc[] +*** xref:amqp/delayed-message-exchange.adoc[] +*** xref:amqp/management-rest-api.adoc[] +*** xref:amqp/exception-handling.adoc[] +*** xref:amqp/transactions.adoc[] +*** xref:amqp/containerAttributes.adoc[] +*** xref:amqp/listener-concurrency.adoc[] +*** xref:amqp/exclusive-consumer.adoc[] +*** xref:amqp/listener-queues.adoc[] +*** xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc[] +*** xref:amqp/multi-rabbit.adoc[] +*** xref:amqp/debugging.adoc[] +** xref:stream.adoc[] +** xref:logging.adoc[] +** xref:sample-apps.adoc[] +** xref:testing.adoc[] +* xref:integration-reference.adoc[] +** xref:si-amqp.adoc[] +* xref:resources.adoc[] +** xref:further-reading.adoc[] +* xref:appendix/micrometer.adoc[] +* xref:appendix/native.adoc[] +* Change History +** xref:appendix/current-release.adoc[] +** xref:appendix/previous-whats-new.adoc[] +*** xref:appendix/previous-whats-new/changes-in-3-0-since-2-4.adoc[] +*** xref:appendix/previous-whats-new/changes-in-2-4-since-2-3.adoc[] +*** xref:appendix/previous-whats-new/message-converter-changes.adoc[] +*** xref:appendix/previous-whats-new/message-converter-changes-1.adoc[] +*** xref:appendix/previous-whats-new/stream-support-changes.adoc[] +*** xref:appendix/previous-whats-new/changes-in-2-3-since-2-2.adoc[] +*** xref:appendix/previous-whats-new/changes-in-2-2-since-2-1.adoc[] +*** xref:appendix/previous-whats-new/changes-in-2-1-since-2-0.adoc[] +*** xref:appendix/previous-whats-new/changes-in-2-0-since-1-7.adoc[] +*** xref:appendix/previous-whats-new/changes-in-1-7-since-1-6.adoc[] +*** xref:appendix/previous-whats-new/earlier-releases.adoc[] +*** xref:appendix/previous-whats-new/changes-in-1-6-since-1-5.adoc[] +*** xref:appendix/previous-whats-new/changes-in-1-5-since-1-4.adoc[] +*** xref:appendix/previous-whats-new/changes-in-1-4-since-1-3.adoc[] +*** xref:appendix/previous-whats-new/changes-in-1-3-since-1-2.adoc[] +*** xref:appendix/previous-whats-new/changes-to-1-2-since-1-1.adoc[] +*** xref:appendix/previous-whats-new/changes-to-1-1-since-1-0.adoc[] diff --git a/src/reference/antora/modules/ROOT/pages/amqp.adoc b/src/reference/antora/modules/ROOT/pages/amqp.adoc new file mode 100644 index 0000000000..79cba0ebaf --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp.adoc @@ -0,0 +1,6 @@ +[[amqp]] += Using Spring AMQP +:page-section-summary-toc: 1 + +This chapter explores the interfaces and classes that are the essential components for developing applications with Spring AMQP. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/abstractions.adoc b/src/reference/antora/modules/ROOT/pages/amqp/abstractions.adoc new file mode 100644 index 0000000000..192c5c7f13 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/abstractions.adoc @@ -0,0 +1,183 @@ +[[amqp-abstractions]] += AMQP Abstractions + +Spring AMQP consists of two modules (each represented by a JAR in the distribution): `spring-amqp` and `spring-rabbit`. +The 'spring-amqp' module contains the `org.springframework.amqp.core` package. +Within that package, you can find the classes that represent the core AMQP "`model`". +Our intention is to provide generic abstractions that do not rely on any particular AMQP broker implementation or client library. +End user code can be more portable across vendor implementations as it can be developed against the abstraction layer only. +These abstractions are then implemented by broker-specific modules, such as 'spring-rabbit'. +There is currently only a RabbitMQ implementation. +However, the abstractions have been validated in .NET using Apache Qpid in addition to RabbitMQ. +Since AMQP operates at the protocol level, in principle, you can use the RabbitMQ client with any broker that supports the same protocol version, but we do not test any other brokers at present. + +This overview assumes that you are already familiar with the basics of the AMQP specification. +If not, have a look at the resources listed in xref:index.adoc#resources[Other Resources] + +[[message]] +== `Message` + +The 0-9-1 AMQP specification does not define a `Message` class or interface. +Instead, when performing an operation such as `basicPublish()`, the content is passed as a byte-array argument and additional properties are passed in as separate arguments. +Spring AMQP defines a `Message` class as part of a more general AMQP domain model representation. +The purpose of the `Message` class is to encapsulate the body and properties within a single instance so that the API can, in turn, be simpler. +The following example shows the `Message` class definition: + +[source,java] +---- +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; + } +} +---- + +The `MessageProperties` interface defines several common properties, such as 'messageId', 'timestamp', 'contentType', and several more. +You can also extend those properties with user-defined 'headers' by calling the `setHeader(String key, Object value)` method. + +IMPORTANT: Starting with versions `1.5.7`, `1.6.11`, `1.7.4`, and `2.0.0`, if a message body is a serialized `Serializable` java object, it is no longer deserialized (by default) when performing `toString()` operations (such as in log messages). +This is to prevent unsafe deserialization. +By default, only `java.util` and `java.lang` classes are deserialized. +To revert to the previous behavior, you can add allowable class/package patterns by invoking `Message.addAllowedListPatterns(...)`. +A simple `*` wildcard is supported, for example `com.something.*, *.MyClass`. +Bodies that cannot be deserialized are represented by `byte[]` in log messages. + +[[exchange]] +== Exchange + +The `Exchange` interface represents an AMQP Exchange, which is what a Message Producer sends to. +Each Exchange within a virtual host of a broker has a unique name as well as a few other properties. +The following example shows the `Exchange` interface: + +[source,java] +---- +public interface Exchange { + + String getName(); + + String getExchangeType(); + + boolean isDurable(); + + boolean isAutoDelete(); + + Map getArguments(); + +} +---- + +As you can see, an `Exchange` also has a 'type' represented by constants defined in `ExchangeTypes`. +The basic types are: `direct`, `topic`, `fanout`, and `headers`. +In the core package, you can find implementations of the `Exchange` interface for each of those types. +The behavior varies across these `Exchange` types in terms of how they handle bindings to queues. +For example, a `Direct` exchange lets a queue be bound by a fixed routing key (often the queue's name). +A `Topic` exchange supports bindings with routing patterns that may include the '*' and '#' wildcards for 'exactly-one' and 'zero-or-more', respectively. +The `Fanout` exchange publishes to all queues that are bound to it without taking any routing key into consideration. +For much more information about these and the other Exchange types, see xref:index.adoc#resources[Other Resources]. + +NOTE: The AMQP specification also requires that any broker provide a "`default`" direct exchange that has no name. +All queues that are declared are bound to that default `Exchange` with their names as routing keys. +You can learn more about the default Exchange's usage within Spring AMQP in xref:amqp/template.adoc[`AmqpTemplate`]. + +[[queue]] +== Queue + +The `Queue` class represents the component from which a message consumer receives messages. +Like the various `Exchange` classes, our implementation is intended to be an abstract representation of this core AMQP type. +The following listing shows the `Queue` class: + +[source,java] +---- +public class Queue { + + private final String name; + + private volatile boolean durable; + + private volatile boolean exclusive; + + private volatile boolean autoDelete; + + private volatile Map 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 + +} +---- + +Notice that the constructor takes the queue name. +Depending on the implementation, the admin template may provide methods for generating a uniquely named queue. +Such queues can be useful as a "`reply-to`" address or in other *temporary* situations. +For that reason, the 'exclusive' and 'autoDelete' properties of an auto-generated queue would both be set to 'true'. + +NOTE: See the section on queues in xref:amqp/broker-configuration.adoc[Configuring the Broker] for information about declaring queues by using namespace support, including queue arguments. + +[[binding]] +== Binding + +Given that a producer sends to an exchange and a consumer receives from a queue, the bindings that connect queues to exchanges are critical for connecting those producers and consumers via messaging. +In Spring AMQP, we define a `Binding` class to represent those connections. +This section reviews the basic options for binding queues to exchanges. + +You can bind a queue to a `DirectExchange` with a fixed routing key, as the following example shows: + +[source,java] +---- +new Binding(someQueue, someDirectExchange, "foo.bar"); +---- + +You can bind a queue to a `TopicExchange` with a routing pattern, as the following example shows: + +[source,java] +---- +new Binding(someQueue, someTopicExchange, "foo.*"); +---- + +You can bind a queue to a `FanoutExchange` with no routing key, as the following example shows: + +[source,java] +---- +new Binding(someQueue, someFanoutExchange); +---- + +We also provide a `BindingBuilder` to facilitate a "`fluent API`" style, as the following example shows: + +[source,java] +---- +Binding b = BindingBuilder.bind(someQueue).to(someTopicExchange).with("foo.*"); +---- + +NOTE: For clarity, the preceding example shows the `BindingBuilder` class, but this style works well when using a static import for the 'bind()' method. + +By itself, an instance of the `Binding` class only holds the data about a connection. +In other words, it is not an "`active`" component. +However, as you will see later in xref:amqp/broker-configuration.adoc[Configuring the Broker], the `AmqpAdmin` class can use `Binding` instances to actually trigger the binding actions on the broker. +Also, as you can see in that same section, you can define the `Binding` instances by using Spring's `@Bean` annotations within `@Configuration` classes. +There is also a convenient base class that further simplifies that approach for generating AMQP-related bean definitions and recognizes the queues, exchanges, and bindings so that they are all declared on the AMQP broker upon application startup. + +The `AmqpTemplate` is also defined within the core package. +As one of the main components involved in actual AMQP messaging, it is discussed in detail in its own section (see xref:amqp/template.adoc[`AmqpTemplate`]). + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc b/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc new file mode 100644 index 0000000000..e3eb3bf916 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc @@ -0,0 +1,682 @@ +[[broker-configuration]] += Configuring the Broker + +The AMQP specification describes how the protocol can be used to configure queues, exchanges, and bindings on the broker. +These operations (which are portable from the 0.8 specification and higher) are present in the `AmqpAdmin` interface in the `org.springframework.amqp.core` package. +The RabbitMQ implementation of that class is `RabbitAdmin` located in the `org.springframework.amqp.rabbit.core` package. + +The `AmqpAdmin` interface is based on using the Spring AMQP domain abstractions and is shown in the following listing: + +[source,java] +---- +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); + +} +---- + +See also xref:amqp/template.adoc#scoped-operations[Scoped Operations]. + +The `getQueueProperties()` method returns some limited information about the queue (message count and consumer count). +The keys for the properties returned are available as constants in the `RabbitTemplate` (`QUEUE_NAME`, +`QUEUE_MESSAGE_COUNT`, and `QUEUE_CONSUMER_COUNT`). +The <> provides much more information in the `QueueInfo` object. + +The no-arg `declareQueue()` method defines a queue on the broker with a name that is automatically generated. +The additional properties of this auto-generated queue are `exclusive=true`, `autoDelete=true`, and `durable=false`. + +The `declareQueue(Queue queue)` method takes a `Queue` object and returns the name of the declared queue. +If the `name` property of the provided `Queue` is an empty `String`, the broker declares the queue with a generated name. +That name is returned to the caller. +That name is also added to the `actualName` property of the `Queue`. +You can use this functionality programmatically only by invoking the `RabbitAdmin` directly. +When using auto-declaration by the admin when defining a queue declaratively in the application context, you can set the name property to `""` (the empty string). +The broker then creates the name. +Starting with version 2.1, listener containers can use queues of this type. +See xref:amqp/containers-and-broker-named-queues.adoc[Containers and Broker-Named queues] for more information. + +This is in contrast to an `AnonymousQueue` where the framework generates a unique (`UUID`) name and sets `durable` to +`false` and `exclusive`, `autoDelete` to `true`. +A `` with an empty (or missing) `name` attribute always creates an `AnonymousQueue`. + +See xref:amqp/broker-configuration.adoc#anonymous-queue[`AnonymousQueue`] to understand why `AnonymousQueue` is preferred over broker-generated queue names as well as +how to control the format of the name. +Starting with version 2.1, anonymous queues are declared with argument `Queue.X_QUEUE_LEADER_LOCATOR` set to `client-local` by default. +This ensures that the queue is declared on the node to which the application is connected. +Declarative queues must have fixed names because they might be referenced elsewhere in the context -- such as in the +listener shown in the following example: + +[source,xml] +---- + + + +---- + +See xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc#automatic-declaration[Automatic Declaration of Exchanges, Queues, and Bindings]. + +The RabbitMQ implementation of this interface is `RabbitAdmin`, which, when configured by using Spring XML, resembles the following example: + +[source,xml] +---- + + + +---- + +When the `CachingConnectionFactory` cache mode is `CHANNEL` (the default), the `RabbitAdmin` implementation does automatic lazy declaration of queues, exchanges, and bindings declared in the same `ApplicationContext`. +These components are declared as soon as a `Connection` is opened to the broker. +There are some namespace features that make this very convenient -- for example, +in the Stocks sample application, we have the following: + +[source,xml] +---- + + + + + + + + + + + + + + + +---- + +In the preceding example, we use anonymous queues (actually, internally, just queues with names generated by the framework, not by the broker) and refer to them by ID. +We can also declare queues with explicit names, which also serve as identifiers for their bean definitions in the context. +The following example configures a queue with an explicit name: + +[source,xml] +---- + +---- + +TIP: You can provide both `id` and `name` attributes. +This lets you refer to the queue (for example, in a binding) by an ID that is independent of the queue name. +It also allows standard Spring features (such as property placeholders and SpEL expressions for the queue name). +These features are not available when you use the name as the bean identifier. + +Queues can be configured with additional arguments -- for example, `x-message-ttl`. +When you use the namespace support, they are provided in the form of a `Map` of argument-name/argument-value pairs, which are defined by using the `` element. +The following example shows how to do so: + +[source,xml] +---- + + + + + + +---- + +By default, the arguments are assumed to be strings. +For arguments of other types, you must provide the type. +The following example shows how to specify the type: + +[source,xml] +---- + + + + + +---- + +When providing arguments of mixed types, you must provide the type for each entry element. +The following example shows how to do so: + +[source,xml] +---- + + + + 100 + + + + + +---- + +With Spring Framework 3.2 and later, this can be declared a little more succinctly, as follows: + +[source,xml] +---- + + + + + + +---- + +When you use Java configuration, the `Queue.X_QUEUE_LEADER_LOCATOR` argument is supported as a first class property through the `setLeaderLocator()` method on the `Queue` class. +Starting with version 2.1, anonymous queues are declared with this property set to `client-local` by default. +This ensures that the queue is declared on the node the application is connected to. + +IMPORTANT: The RabbitMQ broker does not allow declaration of a queue with mismatched arguments. +For example, if a `queue` already exists with no `time to live` argument, and you attempt to declare it with (for example) `key="x-message-ttl" value="100"`, an exception is thrown. + +By default, the `RabbitAdmin` immediately stops processing all declarations when any exception occurs. +This could cause downstream issues, such as a listener container failing to initialize because another queue (defined after the one in error) is not declared. + +This behavior can be modified by setting the `ignore-declaration-exceptions` attribute to `true` on the `RabbitAdmin` instance. +This option instructs the `RabbitAdmin` to log the exception and continue declaring other elements. +When configuring the `RabbitAdmin` using Java, this property is called `ignoreDeclarationExceptions`. +This is a global setting that applies to all elements. +Queues, exchanges, and bindings have a similar property that applies to just those elements. + +Prior to version 1.6, this property took effect only if an `IOException` occurred on the channel, such as when there is a mismatch between current and desired properties. +Now, this property takes effect on any exception, including `TimeoutException` and others. + +In addition, any declaration exceptions result in the publishing of a `DeclarationExceptionEvent`, which is an `ApplicationEvent` that can be consumed by any `ApplicationListener` in the context. +The event contains a reference to the admin, the element that was being declared, and the `Throwable`. + +[[headers-exchange]] +== Headers Exchange + +Starting with version 1.3, you can configure the `HeadersExchange` to match on multiple headers. +You can also specify whether any or all headers must match. +The following example shows how to do so: + +[source,xml] +---- + + + + + + + + + + + +---- + +Starting with version 1.6, you can configure `Exchanges` with an `internal` flag (defaults to `false`) and such an +`Exchange` is properly configured on the Broker through a `RabbitAdmin` (if one is present in the application context). +If the `internal` flag is `true` for an exchange, RabbitMQ does not let clients use the exchange. +This is useful for a dead letter exchange or exchange-to-exchange binding, where you do not wish the exchange to be used +directly by publishers. + +To see how to use Java to configure the AMQP infrastructure, look at the Stock sample application, +where there is the `@Configuration` class `AbstractStockRabbitConfiguration`, which ,in turn has +`RabbitClientConfiguration` and `RabbitServerConfiguration` subclasses. +The following listing shows the code for `AbstractStockRabbitConfiguration`: + +[source,java] +---- +@Configuration +public abstract class AbstractStockAppRabbitConfiguration { + + @Bean + public CachingConnectionFactory 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 Jackson2JsonMessageConverter jsonMessageConverter() { + return new Jackson2JsonMessageConverter(); + } + + @Bean + public TopicExchange marketDataExchange() { + return new TopicExchange("app.stock.marketdata"); + } + + // additional code omitted for brevity + +} +---- + +In the Stock application, the server is configured by using the following `@Configuration` class: + +[source,java] +---- +@Configuration +public class RabbitServerConfiguration extends AbstractStockAppRabbitConfiguration { + + @Bean + public Queue stockRequestQueue() { + return new Queue("app.stock.request"); + } +} +---- + +This is the end of the whole inheritance chain of `@Configuration` classes. +The end result is that `TopicExchange` and `Queue` are declared to the broker upon application startup. +There is no binding of `TopicExchange` to a queue in the server configuration, as that is done in the client application. +The stock request queue, however, is automatically bound to the AMQP default exchange. +This behavior is defined by the specification. + +The client `@Configuration` class is a little more interesting. +Its declaration follows: + +[source,java] +---- +@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 + +} +---- + +The client declares another queue through the `declareQueue()` method on the `AmqpAdmin`. +It binds that queue to the market data exchange with a routing pattern that is externalized in a properties file. + + +[[builder-api]] +== Builder API for Queues and Exchanges + +Version 1.6 introduces a convenient fluent API for configuring `Queue` and `Exchange` objects when using Java configuration. +The following example shows how to use it: + +[source, 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 Javadoc for https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/core/QueueBuilder.html[`org.springframework.amqp.core.QueueBuilder`] and https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/core/ExchangeBuilder.html[`org.springframework.amqp.core.ExchangeBuilder`] for more information. + +Starting with version 2.0, the `ExchangeBuilder` now creates durable exchanges by default, to be consistent with the simple constructors on the individual `AbstractExchange` classes. +To make a non-durable exchange with the builder, use `.durable(false)` before invoking `.build()`. +The `durable()` method with no parameter is no longer provided. + +Version 2.2 introduced fluent APIs to add "well known" exchange and queue arguments... + +[source, java] +---- +@Bean +public Queue allArgs1() { + return QueueBuilder.nonDurable("all.args.1") + .ttl(1000) + .expires(200_000) + .maxLength(42) + .maxLengthBytes(10_000) + .overflow(Overflow.rejectPublish) + .deadLetterExchange("dlx") + .deadLetterRoutingKey("dlrk") + .maxPriority(4) + .lazy() + .leaderLocator(LeaderLocator.minLeaders) + .singleActiveConsumer() + .build(); +} + +@Bean +public DirectExchange ex() { + return ExchangeBuilder.directExchange("ex.with.alternate") + .durable(true) + .alternate("alternate") + .build(); +} +---- + +[[collection-declaration]] +== Declaring Collections of Exchanges, Queues, and Bindings + +You can wrap collections of `Declarable` objects (`Queue`, `Exchange`, and `Binding`) in `Declarables` objects. +The `RabbitAdmin` detects such beans (as well as discrete `Declarable` beans) in the application context, and declares the contained objects on the broker whenever a connection is established (initially and after a connection failure). +The following example shows how to do so: + +[source, java] +---- +@Configuration +public static class Config { + + @Bean + public CachingConnectionFactory 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 Declarables es() { + return new Declarables( + new DirectExchange("e2", false, true), + new DirectExchange("e3", false, true)); + } + + @Bean + public Declarables qs() { + return new Declarables( + new Queue("q2", false, false, true), + new Queue("q3", false, false, true)); + } + + @Bean + @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) + public Declarables prototypes() { + return new Declarables(new Queue(this.prototypeQueueName, false, false, true)); + } + + @Bean + public Declarables bs() { + return new Declarables( + new Binding("q2", DestinationType.QUEUE, "e2", "k2", null), + new Binding("q3", DestinationType.QUEUE, "e3", "k3", null)); + } + + @Bean + public Declarables ds() { + return new Declarables( + new DirectExchange("e4", false, true), + new Queue("q4", false, false, true), + new Binding("q4", DestinationType.QUEUE, "e4", "k4", null)); + } + +} +---- + +IMPORTANT: In versions prior to 2.1, you could declare multiple `Declarable` instances by defining beans of type `Collection`. +This can cause undesirable side effects in some cases, because the admin has to iterate over all `Collection` beans. + +Version 2.2 added the `getDeclarablesByType` method to `Declarables`; this can be used as a convenience, for example, when declaring the listener container bean(s). + +[source, java] +---- +public SimpleMessageListenerContainer container(ConnectionFactory connectionFactory, + Declarables mixedDeclarables, MessageListener listener) { + + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); + container.setQueues(mixedDeclarables.getDeclarablesByType(Queue.class).toArray(new Queue[0])); + container.setMessageListener(listener); + return container; +} +---- + +[[conditional-declaration]] +== Conditional Declaration + +By default, all queues, exchanges, and bindings are declared by all `RabbitAdmin` instances (assuming they have `auto-startup="true"`) in the application context. + +Starting with version 2.1.9, the `RabbitAdmin` has a new property `explicitDeclarationsOnly` (which is `false` by default); when this is set to `true`, the admin will only declare beans that are explicitly configured to be declared by that admin. + +NOTE: Starting with the 1.2 release, you can conditionally declare these elements. +This is particularly useful when an application connects to multiple brokers and needs to specify with which brokers a particular element should be declared. + +The classes representing these elements implement `Declarable`, which has two methods: `shouldDeclare()` and `getDeclaringAdmins()`. +The `RabbitAdmin` uses these methods to determine whether a particular instance should actually process the declarations on its `Connection`. + +The properties are available as attributes in the namespace, as shown in the following examples: + +[source,xml] +---- + + + + + + + + + + + + + + + + + + + +---- + +NOTE: By default, the `auto-declare` attribute is `true` and, if the `declared-by` is not supplied (or is empty), then all `RabbitAdmin` instances declare the object (as long as the admin's `auto-startup` attribute is `true`, the default, and the admin's `explicit-declarations-only` attribute is false). + +Similarly, you can use Java-based `@Configuration` to achieve the same effect. +In the following example, the components are declared by `admin1` but not by `admin2`: + +[source,java] +---- +@Bean +public RabbitAdmin admin1() { + return new RabbitAdmin(cf1()); +} + +@Bean +public RabbitAdmin admin2() { + return new RabbitAdmin(cf2()); +} + +@Bean +public Queue queue() { + Queue queue = new Queue("foo"); + queue.setAdminsThatShouldDeclare(admin1()); + return queue; +} + +@Bean +public Exchange exchange() { + DirectExchange exchange = new DirectExchange("bar"); + exchange.setAdminsThatShouldDeclare(admin1()); + return exchange; +} + +@Bean +public Binding binding() { + Binding binding = new Binding("foo", DestinationType.QUEUE, exchange().getName(), "foo", null); + binding.setAdminsThatShouldDeclare(admin1()); + return binding; +} +---- + +[[note-id-name]] +== A Note On the `id` and `name` Attributes + +The `name` attribute on `` and `` elements reflects the name of the entity in the broker. +For queues, if the `name` is omitted, an anonymous queue is created (see xref:amqp/broker-configuration.adoc#anonymous-queue[`AnonymousQueue`]). + +In versions prior to 2.0, the `name` was also registered as a bean name alias (similar to `name` on `` elements). + +This caused two problems: + +* It prevented the declaration of a queue and exchange with the same name. +* The alias was not resolved if it contained a SpEL expression (`#{...}`). + +Starting with version 2.0, if you declare one of these elements with both an `id` _and_ a `name` attribute, the name is no longer declared as a bean name alias. +If you wish to declare a queue and exchange with the same `name`, you must provide an `id`. + +There is no change if the element has only a `name` attribute. +The bean can still be referenced by the `name` -- for example, in binding declarations. +However, you still cannot reference it if the name contains SpEL -- you must provide an `id` for reference purposes. + + +[[anonymous-queue]] +== `AnonymousQueue` + +In general, when you need a uniquely-named, exclusive, auto-delete queue, we recommend that you use the `AnonymousQueue` +instead of broker-defined queue names (using `""` as a `Queue` name causes the broker to generate the queue +name). + +This is because: + +. The queues are actually declared when the connection to the broker is established. +This is long after the beans are created and wired together. +Beans that use the queue need to know its name. +In fact, the broker might not even be running when the application is started. +. If the connection to the broker is lost for some reason, the admin re-declares the `AnonymousQueue` with the same name. +If we used broker-declared queues, the queue name would change. + +You can control the format of the queue name used by `AnonymousQueue` instances. + +By default, the queue name is prefixed by `spring.gen-` followed by a base64 representation of the `UUID` -- for example: `spring.gen-MRBv9sqISkuCiPfOYfpo4g`. + +You can provide an `AnonymousQueue.NamingStrategy` implementation in a constructor argument. +The following example shows how to do so: + +[source, java] +---- +@Bean +public Queue anon1() { + return new AnonymousQueue(); +} + +@Bean +public Queue anon2() { + return new AnonymousQueue(new AnonymousQueue.Base64UrlNamingStrategy("something-")); +} + +@Bean +public Queue anon3() { + return new AnonymousQueue(AnonymousQueue.UUIDNamingStrategy.DEFAULT); +} +---- + +The first bean generates a queue name prefixed by `spring.gen-` followed by a base64 representation of the `UUID` -- for +example: `spring.gen-MRBv9sqISkuCiPfOYfpo4g`. +The second bean generates a queue name prefixed by `something-` followed by a base64 representation of the `UUID`. +The third bean generates a name by using only the UUID (no base64 conversion) -- for example, `f20c818a-006b-4416-bf91-643590fedb0e`. + +The base64 encoding uses the "`URL and Filename Safe Alphabet`" from RFC 4648. +Trailing padding characters (`=`) are removed. + +You can provide your own naming strategy, whereby you can include other information (such as the application name or client host) in the queue name. + +You can specify the naming strategy when you use XML configuration. +The `naming-strategy` attribute is present on the `` element +for a bean reference that implements `AnonymousQueue.NamingStrategy`. +The following examples show how to specify the naming strategy in various ways: + +[source, xml] +---- + + + + + + + + + + + +---- + +The first example creates names such as `spring.gen-MRBv9sqISkuCiPfOYfpo4g`. +The second example creates names with a String representation of a UUID. +The third example creates names such as `custom.gen-MRBv9sqISkuCiPfOYfpo4g`. + +You can also provide your own naming strategy bean. + +Starting with version 2.1, anonymous queues are declared with argument `Queue.X_QUEUE_LEADER_LOCATOR` set to `client-local` by default. +This ensures that the queue is declared on the node to which the application is connected. +You can revert to the previous behavior by calling `queue.setLeaderLocator(null)` after constructing the instance. + +[[declarable-recovery]] +== Recovering Auto-Delete Declarations + +Normally, the `RabbitAdmin` (s) only recover queues/exchanges/bindings that are declared as beans in the application context; if any such declarations are auto-delete, they will be removed by the broker if the connection is lost. +When the connection is re-established, the admin will redeclare the entities. +Normally, entities created by calling `admin.declareQueue(...)`, `admin.declareExchange(...)` and `admin.declareBinding(...)` will not be recovered. + +Starting with version 2.4, the admin has a new property `redeclareManualDeclarations`; when `true`, the admin will recover these entities in addition to the beans in the application context. + +Recovery of individual declarations will not be performed if `deleteQueue(...)`, `deleteExchange(...)` or `removeBinding(...)` is called. +Associated bindings are removed from the recoverable entities when queues and exchanges are deleted. + +Finally, calling `resetAllManualDeclarations()` will prevent the recovery of any previously declared entities. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/broker-events.adoc b/src/reference/antora/modules/ROOT/pages/amqp/broker-events.adoc new file mode 100644 index 0000000000..ab9ad77b8d --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/broker-events.adoc @@ -0,0 +1,26 @@ +[[broker-events]] += Broker Event Listener + +When the https://www.rabbitmq.com/event-exchange.html[Event Exchange Plugin] is enabled, if you add a bean of type `BrokerEventListener` to the application context, it publishes selected broker events as `BrokerEvent` instances, which can be consumed with a normal Spring `ApplicationListener` or `@EventListener` method. +Events are published by the broker to a topic exchange `amq.rabbitmq.event` with a different routing key for each event type. +The listener uses event keys, which are used to bind an `AnonymousQueue` to the exchange so the listener receives only selected events. +Since it is a topic exchange, wildcards can be used (as well as explicitly requesting specific events), as the following example shows: + +[source, java] +---- +@Bean +public BrokerEventListener eventListener() { + return new BrokerEventListener(connectionFactory(), "user.deleted", "channel.#", "queue.#"); +} +---- + +You can further narrow the received events in individual event listeners, by using normal Spring techniques, as the following example shows: + +[source, java] +---- +@EventListener(condition = "event.eventType == 'queue.created'") +public void listener(BrokerEvent event) { + ... +} +---- + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc b/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc new file mode 100644 index 0000000000..d2199ecddd --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc @@ -0,0 +1,839 @@ +[[connections]] += Connection and Resource Management + +Whereas the AMQP model we described in the previous section is generic and applicable to all implementations, when we get into the management of resources, the details are specific to the broker implementation. +Therefore, in this section, we focus on code that exists only within our "`spring-rabbit`" module since, at this point, RabbitMQ is the only supported implementation. + +The central component for managing a connection to the RabbitMQ broker is the `ConnectionFactory` interface. +The responsibility of a `ConnectionFactory` implementation is to provide an instance of `org.springframework.amqp.rabbit.connection.Connection`, which is a wrapper for `com.rabbitmq.client.Connection`. + +[[choosing-factory]] +== Choosing a Connection Factory + +There are three connection factories to chose from + +* `PooledChannelConnectionFactory` +* `ThreadChannelConnectionFactory` +* `CachingConnectionFactory` + +The first two were added in version 2.3. + +For most use cases, the `CachingConnectionFactory` should be used. +The `ThreadChannelConnectionFactory` can be used if you want to ensure strict message ordering without the need to use xref:amqp/template.adoc#scoped-operations[Scoped Operations]. +The `PooledChannelConnectionFactory` is similar to the `CachingConnectionFactory` in that it uses a single connection and a pool of channels. +It's implementation is simpler but it doesn't support correlated publisher confirmations. + +Simple publisher confirmations are supported by all three factories. + +When configuring a `RabbitTemplate` to use a xref:amqp/template.adoc#separate-connection[separate connection], you can now, starting with version 2.3.2, configure the publishing connection factory to be a different type. +By default, the publishing factory is the same type and any properties set on the main factory are also propagated to the publishing factory. + +[[pooledchannelconnectionfactory]] +=== `PooledChannelConnectionFactory` + +This factory manages a single connection and two pools of channels, based on the Apache Pool2. +One pool is for transactional channels, the other is for non-transactional channels. +The pools are `GenericObjectPool` s with default configuration; a callback is provided to configure the pools; refer to the Apache documentation for more information. + +The Apache `commons-pool2` jar must be on the class path to use this factory. + +[source, java] +---- +@Bean +PooledChannelConnectionFactory pcf() throws Exception { + ConnectionFactory rabbitConnectionFactory = new ConnectionFactory(); + rabbitConnectionFactory.setHost("localhost"); + PooledChannelConnectionFactory pcf = new PooledChannelConnectionFactory(rabbitConnectionFactory); + pcf.setPoolConfigurer((pool, tx) -> { + if (tx) { + // configure the transactional pool + } + else { + // configure the non-transactional pool + } + }); + return pcf; +} +---- + +[[threadchannelconnectionfactory]] +=== `ThreadChannelConnectionFactory` + +This factory manages a single connection and two `ThreadLocal` s, one for transactional channels, the other for non-transactional channels. +This factory ensures that all operations on the same thread use the same channel (as long as it remains open). +This facilitates strict message ordering without the need for xref:amqp/template.adoc#scoped-operations[Scoped Operations]. +To avoid memory leaks, if your application uses many short-lived threads, you must call the factory's `closeThreadChannel()` to release the channel resource. +Starting with version 2.3.7, a thread can transfer its channel(s) to another thread. +See xref:amqp/template.adoc#multi-strict[Strict Message Ordering in a Multi-Threaded Environment] for more information. + +[[cachingconnectionfactory]] +=== `CachingConnectionFactory` + +The third implementation provided is the `CachingConnectionFactory`, which, by default, establishes a single connection proxy that can be shared by the application. +Sharing of the connection is possible since the "`unit of work`" for messaging with AMQP is actually a "`channel`" (in some ways, this is similar to the relationship between a connection and a session in JMS). +The connection instance provides a `createChannel` method. +The `CachingConnectionFactory` implementation supports caching of those channels, and it maintains separate caches for channels based on whether they are transactional. +When creating an instance of `CachingConnectionFactory`, you can provide the 'hostname' through the constructor. +You should also provide the 'username' and 'password' properties. +To configure the size of the channel cache (the default is 25), you can call the +`setChannelCacheSize()` method. + +Starting with version 1.3, you can configure the `CachingConnectionFactory` to cache connections as well as only channels. +In this case, each call to `createConnection()` creates a new connection (or retrieves an idle one from the cache). +Closing a connection returns it to the cache (if the cache size has not been reached). +Channels created on such connections are also cached. +The use of separate connections might be useful in some environments, such as consuming from an HA cluster, in +conjunction with a load balancer, to connect to different cluster members, and others. +To cache connections, set the `cacheMode` to `CacheMode.CONNECTION`. + +NOTE: This does not limit the number of connections. +Rather, it specifies how many idle open connections are allowed. + +Starting with version 1.5.5, a new property called `connectionLimit` is provided. +When this property is set, it limits the total number of connections allowed. +When set, if the limit is reached, the `channelCheckoutTimeLimit` is used to wait for a connection to become idle. +If the time is exceeded, an `AmqpTimeoutException` is thrown. + +[IMPORTANT] +====== +When the cache mode is `CONNECTION`, automatic declaration of queues and others +(See xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc#automatic-declaration[Automatic Declaration of Exchanges, Queues, and Bindings]) is NOT supported. + +Also, at the time of this writing, the `amqp-client` library by default creates a fixed thread pool for each connection (default size: `Runtime.getRuntime().availableProcessors() * 2` threads). +When using a large number of connections, you should consider setting a custom `executor` on the `CachingConnectionFactory`. +Then, the same executor can be used by all connections and its threads can be shared. +The executor's thread pool should be unbounded or set appropriately for the expected use (usually, at least one thread per connection). +If multiple channels are created on each connection, the pool size affects the concurrency, so a variable (or simple cached) thread pool executor would be most suitable. +====== + +It is important to understand that the cache size is (by default) not a limit but is merely the number of channels that can be cached. +With a cache size of, say, 10, any number of channels can actually be in use. +If more than 10 channels are being used and they are all returned to the cache, 10 go in the cache. +The remainder are physically closed. + +Starting with version 1.6, the default channel cache size has been increased from 1 to 25. +In high volume, multi-threaded environments, a small cache means that channels are created and closed at a high rate. +Increasing the default cache size can avoid this overhead. +You should monitor the channels in use through the RabbitMQ Admin UI and consider increasing the cache size further if you +see many channels being created and closed. +The cache grows only on-demand (to suit the concurrency requirements of the application), so this change does not +impact existing low-volume applications. + +Starting with version 1.4.2, the `CachingConnectionFactory` has a property called `channelCheckoutTimeout`. +When this property is greater than zero, the `channelCacheSize` becomes a limit on the number of channels that can be created on a connection. +If the limit is reached, calling threads block until a channel is available or this timeout is reached, in which case a `AmqpTimeoutException` is thrown. + +WARNING: Channels used within the framework (for example, +`RabbitTemplate`) are reliably returned to the cache. +If you create channels outside of the framework, (for example, +by accessing the connections directly and invoking `createChannel()`), you must return them (by closing) reliably, perhaps in a `finally` block, to avoid running out of channels. + +The following example shows how to create a new `connection`: + +[source,java] +---- +CachingConnectionFactory connectionFactory = new CachingConnectionFactory("somehost"); +connectionFactory.setUsername("guest"); +connectionFactory.setPassword("guest"); + +Connection connection = connectionFactory.createConnection(); +---- + +When using XML, the configuration might look like the following example: + +[source,xml] +---- + + + + + +---- + +NOTE: There is also a `SingleConnectionFactory` implementation that is available only in the unit test code of the framework. +It is simpler than `CachingConnectionFactory`, since it does not cache channels, but it is not intended for practical usage outside of simple tests due to its lack of performance and resilience. +If you need to implement your own `ConnectionFactory` for some reason, the `AbstractConnectionFactory` base class may provide a nice starting point. + +A `ConnectionFactory` can be created quickly and conveniently by using the rabbit namespace, as follows: + +[source,xml] +---- + +---- + +In most cases, this approach is preferable, since the framework can choose the best defaults for you. +The created instance is a `CachingConnectionFactory`. +Keep in mind that the default cache size for channels is 25. +If you want more channels to be cached, set a larger value by setting the 'channelCacheSize' property. +In XML it would look like as follows: + +[source,xml] +---- + + + + + + +---- + +Also, with the namespace, you can add the 'channel-cache-size' attribute, as follows: + +[source,xml] +---- + +---- + +The default cache mode is `CHANNEL`, but you can configure it to cache connections instead. +In the following example, we use `connection-cache-size`: + +[source,xml] +---- + +---- + +You can provide host and port attributes by using the namespace, as follows: + +[source,xml] +---- + +---- + +Alternatively, if running in a clustered environment, you can use the addresses attribute, as follows: + +[source,xml] +---- + +---- + +See xref:amqp/connections.adoc#cluster[Connecting to a Cluster] for information about `address-shuffle-mode`. + +The following example with a custom thread factory that prefixes thread names with `rabbitmq-`: + +[source, xml] +---- + + + + + + +---- + +[[addressresolver]] +== AddressResolver + +Starting with version 2.1.15, you can now use an `AddressResolver` to resolve the connection address(es). +This will override any settings of the `addresses` and `host/port` properties. + +[[naming-connections]] +== Naming Connections + +Starting with version 1.7, a `ConnectionNameStrategy` is provided for the injection into the `AbstractionConnectionFactory`. +The generated name is used for the application-specific identification of the target RabbitMQ connection. +The connection name is displayed in the management UI if the RabbitMQ server supports it. +This value does not have to be unique and cannot be used as a connection identifier -- for example, in HTTP API requests. +This value is supposed to be human-readable and is a part of `ClientProperties` under the `connection_name` key. +You can use a simple Lambda, as follows: + +[source, java] +---- +connectionFactory.setConnectionNameStrategy(connectionFactory -> "MY_CONNECTION"); +---- + +The `ConnectionFactory` argument can be used to distinguish target connection names by some logic. +By default, the `beanName` of the `AbstractConnectionFactory`, a hex string representing the object, and an internal counter are used to generate the `connection_name`. +The `` namespace component is also supplied with the `connection-name-strategy` attribute. + +An implementation of `SimplePropertyValueConnectionNameStrategy` sets the connection name to an application property. +You can declare it as a `@Bean` and inject it into the connection factory, as the following example shows: + +[source, java] +---- +@Bean +public SimplePropertyValueConnectionNameStrategy cns() { + return new SimplePropertyValueConnectionNameStrategy("spring.application.name"); +} + +@Bean +public ConnectionFactory rabbitConnectionFactory(ConnectionNameStrategy cns) { + CachingConnectionFactory connectionFactory = new CachingConnectionFactory(); + ... + connectionFactory.setConnectionNameStrategy(cns); + return connectionFactory; +} +---- + +The property must exist in the application context's `Environment`. + +NOTE: When using Spring Boot and its autoconfigured connection factory, you need only declare the `ConnectionNameStrategy` `@Bean`. +Boot auto-detects the bean and wires it into the factory. + +[[blocked-connections-and-resource-constraints]] +== Blocked Connections and Resource Constraints + +The connection might be blocked for interaction from the broker that corresponds to the https://www.rabbitmq.com/memory.html[Memory Alarm]. +Starting with version 2.0, the `org.springframework.amqp.rabbit.connection.Connection` can be supplied with `com.rabbitmq.client.BlockedListener` instances to be notified for connection blocked and unblocked events. +In addition, the `AbstractConnectionFactory` emits a `ConnectionBlockedEvent` and `ConnectionUnblockedEvent`, respectively, through its internal `BlockedListener` implementation. +These let you provide application logic to react appropriately to problems on the broker and (for example) take some corrective actions. + +IMPORTANT: When the application is configured with a single `CachingConnectionFactory`, as it is by default with Spring Boot auto-configuration, the application stops working when the connection is blocked by the Broker. +And when it is blocked by the Broker, any of its clients stop to work. +If we have producers and consumers in the same application, we may end up with a deadlock when producers are blocking the connection (because there are no resources on the Broker any more) and consumers cannot free them (because the connection is blocked). +To mitigate the problem, we suggest having one more separate `CachingConnectionFactory` instance with the same options -- one for producers and one for consumers. +A separate `CachingConnectionFactory` is not possible for transactional producers that execute on a consumer thread, since they should reuse the `Channel` associated with the consumer transactions. + +Starting with version 2.0.2, the `RabbitTemplate` has a configuration option to automatically use a second connection factory, unless transactions are being used. +See xref:amqp/template.adoc#separate-connection[Using a Separate Connection] for more information. +The `ConnectionNameStrategy` for the publisher connection is the same as the primary strategy with `.publisher` appended to the result of calling the method. + +Starting with version 1.7.7, an `AmqpResourceNotAvailableException` is provided, which is thrown when `SimpleConnection.createChannel()` cannot create a `Channel` (for example, because the `channelMax` limit is reached and there are no available channels in the cache). +You can use this exception in the `RetryPolicy` to recover the operation after some back-off. + +[[connection-factory]] +== Configuring the Underlying Client Connection Factory + +The `CachingConnectionFactory` uses an instance of the Rabbit client `ConnectionFactory`. +A number of configuration properties are passed through (`host`, `port`, `userName`, `password`, `requestedHeartBeat`, and `connectionTimeout` for example) when setting the equivalent property on the `CachingConnectionFactory`. +To set other properties (`clientProperties`, for example), you can define an instance of the Rabbit factory and provide a reference to it by using the appropriate constructor of the `CachingConnectionFactory`. +When using the namespace (xref:amqp/connections.adoc[as described earlier]), you need to provide a reference to the configured factory in the `connection-factory` attribute. +For convenience, a factory bean is provided to assist in configuring the connection factory in a Spring application context, as discussed in xref:amqp/connections.adoc#rabbitconnectionfactorybean-configuring-ssl[the next section]. + +[source,xml] +---- + +---- + +NOTE: The 4.0.x client enables automatic recovery by default. +While compatible with this feature, Spring AMQP has its own recovery mechanisms and the client recovery feature generally is not needed. +We recommend disabling `amqp-client` automatic recovery, to avoid getting `AutoRecoverConnectionNotCurrentlyOpenException` instances when the broker is available but the connection has not yet recovered. +You may notice this exception, for example, when a `RetryTemplate` is configured in a `RabbitTemplate`, even when failing over to another broker in a cluster. +Since the auto-recovering connection recovers on a timer, the connection may be recovered more quickly by using Spring AMQP's recovery mechanisms. +Starting with version 1.7.1, Spring AMQP disables `amqp-client` automatic recovery unless you explicitly create your own RabbitMQ connection factory and provide it to the `CachingConnectionFactory`. +RabbitMQ `ConnectionFactory` instances created by the `RabbitConnectionFactoryBean` also have the option disabled by default. + +[[rabbitconnectionfactorybean-configuring-ssl]] +== `RabbitConnectionFactoryBean` and Configuring SSL + +Starting with version 1.4, a convenient `RabbitConnectionFactoryBean` is provided to enable convenient configuration of SSL properties on the underlying client connection factory by using dependency injection. +Other setters delegate to the underlying factory. +Previously, you had to configure the SSL options programmatically. +The following example shows how to configure a `RabbitConnectionFactoryBean`: + +[source,java,role=primary] +.Java +---- +@Bean +RabbitConnectionFactoryBean rabbitConnectionFactory() { + RabbitConnectionFactoryBean factoryBean = new RabbitConnectionFactoryBean(); + factoryBean.setUseSSL(true); + factoryBean.setSslPropertiesLocation(new ClassPathResource("secrets/rabbitSSL.properties")); + return factoryBean; +} + +@Bean +CachingConnectionFactory connectionFactory(ConnectionFactory rabbitConnectionFactory) { + CachingConnectionFactory ccf = new CachingConnectionFactory(rabbitConnectionFactory); + ccf.setHost("..."); + // ... + return ccf; +} +---- +[source,properties,role=secondary] +.Boot application.properties +---- +spring.rabbitmq.ssl.enabled:true +spring.rabbitmq.ssl.keyStore=... +spring.rabbitmq.ssl.keyStoreType=jks +spring.rabbitmq.ssl.keyStorePassword=... +spring.rabbitmq.ssl.trustStore=... +spring.rabbitmq.ssl.trustStoreType=jks +spring.rabbitmq.ssl.trustStorePassword=... +spring.rabbitmq.host=... +... +---- +[source,xml,role=secondary] +.XML +---- + + + + + + +---- + +See the https://www.rabbitmq.com/ssl.html[RabbitMQ Documentation] for information about configuring SSL. +Omit the `keyStore` and `trustStore` configuration to connect over SSL without certificate validation. +The next example shows how you can provide key and trust store configuration. + +The `sslPropertiesLocation` property is a Spring `Resource` pointing to a properties file containing the following keys: + +[source] +---- +keyStore=file:/secret/keycert.p12 +trustStore=file:/secret/trustStore +keyStore.passPhrase=secret +trustStore.passPhrase=secret +---- + +The `keyStore` and `truststore` are Spring `Resources` pointing to the stores. +Typically this properties file is secured by the operating system with the application having read access. + +Starting with Spring AMQP version 1.5,you can set these properties directly on the factory bean. +If both discrete properties and `sslPropertiesLocation` is provided, properties in the latter override the +discrete values. + +IMPORTANT: Starting with version 2.0, the server certificate is validated by default because it is more secure. +If you wish to skip this validation for some reason, set the factory bean's `skipServerCertificateValidation` property to `true`. +Starting with version 2.1, the `RabbitConnectionFactoryBean` now calls `enableHostnameVerification()` by default. +To revert to the previous behavior, set the `enableHostnameVerification` property to `false`. + +IMPORTANT: Starting with version 2.2.5, the factory bean will always use TLS v1.2 by default; previously, it used v1.1 in some cases and v1.2 in others (depending on other properties). +If you need to use v1.1 for some reason, set the `sslAlgorithm` property: `setSslAlgorithm("TLSv1.1")`. + +[[cluster]] +== Connecting to a Cluster + +To connect to a cluster, configure the `addresses` property on the `CachingConnectionFactory`: + +[source, java] +---- +@Bean +public CachingConnectionFactory ccf() { + CachingConnectionFactory ccf = new CachingConnectionFactory(); + ccf.setAddresses("host1:5672,host2:5672,host3:5672"); + return ccf; +} +---- + +Starting with version 3.0, the underlying connection factory will attempt to connect to a host, by choosing a random address, whenever a new connection is established. +To revert to the previous behavior of attempting to connect from first to last, set the `addressShuffleMode` property to `AddressShuffleMode.NONE`. + +Starting with version 2.3, the `INORDER` shuffle mode was added, which means the first address is moved to the end after a connection is created. +You may wish to use this mode with the https://github.com/rabbitmq/rabbitmq-sharding[RabbitMQ Sharding Plugin] with `CacheMode.CONNECTION` and suitable concurrency if you wish to consume from all shards on all nodes. + +[source, java] +---- +@Bean +public CachingConnectionFactory ccf() { + CachingConnectionFactory ccf = new CachingConnectionFactory(); + ccf.setAddresses("host1:5672,host2:5672,host3:5672"); + ccf.setAddressShuffleMode(AddressShuffleMode.INORDER); + return ccf; +} +---- + +[[routing-connection-factory]] +== Routing Connection Factory + +Starting with version 1.3, the `AbstractRoutingConnectionFactory` has been introduced. +This factory provides a mechanism to configure mappings for several `ConnectionFactories` and determine a target `ConnectionFactory` by some `lookupKey` at runtime. +Typically, the implementation checks a thread-bound context. +For convenience, Spring AMQP provides the `SimpleRoutingConnectionFactory`, which gets the current thread-bound `lookupKey` from the `SimpleResourceHolder`. +The following examples shows how to configure a `SimpleRoutingConnectionFactory` in both XML and Java: + +[source,xml] +---- + + + + + + + + + + +---- + +[source,java] +---- +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()); + } + +} +---- + +It is important to unbind the resource after use. +For more information, see the https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.html[JavaDoc] for `AbstractRoutingConnectionFactory`. + +Starting with version 1.4, `RabbitTemplate` supports the SpEL `sendConnectionFactorySelectorExpression` and `receiveConnectionFactorySelectorExpression` properties, which are evaluated on each AMQP protocol interaction operation (`send`, `sendAndReceive`, `receive`, or `receiveAndReply`), resolving to a `lookupKey` value for the provided `AbstractRoutingConnectionFactory`. +You can use bean references, such as `@vHostResolver.getVHost(#root)` in the expression. +For `send` operations, the message to be sent is the root evaluation object. +For `receive` operations, the `queueName` is the root evaluation object. + +The routing algorithm is as follows: If the selector expression is `null` or is evaluated to `null` or the provided `ConnectionFactory` is not an instance of `AbstractRoutingConnectionFactory`, everything works as before, relying on the provided `ConnectionFactory` implementation. +The same occurs if the evaluation result is not `null`, but there is no target `ConnectionFactory` for that `lookupKey` and the `AbstractRoutingConnectionFactory` is configured with `lenientFallback = true`. +In the case of an `AbstractRoutingConnectionFactory`, it does fallback to its `routing` implementation based on `determineCurrentLookupKey()`. +However, if `lenientFallback = false`, an `IllegalStateException` is thrown. + +The namespace support also provides the `send-connection-factory-selector-expression` and `receive-connection-factory-selector-expression` attributes on the `` component. + +Also, starting with version 1.4, you can configure a routing connection factory in a listener container. +In that case, the list of queue names is used as the lookup key. +For example, if you configure the container with `setQueueNames("thing1", "thing2")`, the lookup key is `[thing1,thing]"` (note that there is no space in the key). + +Starting with version 1.6.9, you can add a qualifier to the lookup key by using `setLookupKeyQualifier` on the listener container. +Doing so enables, for example, listening to queues with the same name but in a different virtual host (where you would have a connection factory for each). + +For example, with lookup key qualifier `thing1` and a container listening to queue `thing2`, the lookup key you could register the target connection factory with could be `thing1[thing2]`. + +IMPORTANT: The target (and default, if provided) connection factories must have the same settings for publisher confirms and returns. +See xref:amqp/connections.adoc#cf-pub-conf-ret[Publisher Confirms and Returns]. + +Starting with version 2.4.4, this validation can be disabled. +If you have a case that the values between confirms and returns need to be unequal, you can use `AbstractRoutingConnectionFactory#setConsistentConfirmsReturns` to turn of the validation. +Note that the first connection factory added to `AbstractRoutingConnectionFactory` will determine the general values of `confirms` and `returns`. + +It may be useful if you have a case that certain messages you would to check confirms/returns and others you don't. +For example: + +[source, java] +---- +@Bean +public RabbitTemplate rabbitTemplate() { + final com.rabbitmq.client.ConnectionFactory cf = new com.rabbitmq.client.ConnectionFactory(); + cf.setHost("localhost"); + cf.setPort(5672); + + CachingConnectionFactory cachingConnectionFactory = new CachingConnectionFactory(cf); + cachingConnectionFactory.setPublisherConfirmType(CachingConnectionFactory.ConfirmType.CORRELATED); + + PooledChannelConnectionFactory pooledChannelConnectionFactory = new PooledChannelConnectionFactory(cf); + + final Map connectionFactoryMap = new HashMap<>(2); + connectionFactoryMap.put("true", cachingConnectionFactory); + connectionFactoryMap.put("false", pooledChannelConnectionFactory); + + final AbstractRoutingConnectionFactory routingConnectionFactory = new SimpleRoutingConnectionFactory(); + routingConnectionFactory.setConsistentConfirmsReturns(false); + routingConnectionFactory.setDefaultTargetConnectionFactory(pooledChannelConnectionFactory); + routingConnectionFactory.setTargetConnectionFactories(connectionFactoryMap); + + final RabbitTemplate rabbitTemplate = new RabbitTemplate(routingConnectionFactory); + + final Expression sendExpression = new SpelExpressionParser().parseExpression( + "messageProperties.headers['x-use-publisher-confirms'] ?: false"); + rabbitTemplate.setSendConnectionFactorySelectorExpression(sendExpression); +} +---- + +This way messages with the header `x-use-publisher-confirms: true` will be sent through the caching connection and you can ensure the message delivery. +See xref:amqp/connections.adoc#cf-pub-conf-ret[Publisher Confirms and Returns] for more information about ensuring message delivery. + +[[queue-affinity]] +== Queue Affinity and the `LocalizedQueueConnectionFactory` + +When using HA queues in a cluster, for the best performance, you may want to connect to the physical broker +where the lead queue resides. +The `CachingConnectionFactory` can be configured with multiple broker addresses. +This is to fail over and the client attempts to connect in accordance with the configured `AddressShuffleMode` order. +The `LocalizedQueueConnectionFactory` uses the REST API provided by the management plugin to determine which node is the lead for the queue. +It then creates (or retrieves from a cache) a `CachingConnectionFactory` that connects to just that node. +If the connection fails, the new lead node is determined and the consumer connects to it. +The `LocalizedQueueConnectionFactory` is configured with a default connection factory, in case the physical location of the queue cannot be determined, in which case it connects as normal to the cluster. + +The `LocalizedQueueConnectionFactory` is a `RoutingConnectionFactory` and the `SimpleMessageListenerContainer` uses the queue names as the lookup key as discussed in <> above. + +NOTE: For this reason (the use of the queue name for the lookup), the `LocalizedQueueConnectionFactory` can only be used if the container is configured to listen to a single queue. + +NOTE: The RabbitMQ management plugin must be enabled on each node. + +CAUTION: This connection factory is intended for long-lived connections, such as those used by the `SimpleMessageListenerContainer`. +It is not intended for short connection use, such as with a `RabbitTemplate` because of the overhead of invoking the REST API before making the connection. +Also, for publish operations, the queue is unknown, and the message is published to all cluster members anyway, so the logic of looking up the node has little value. + +The following example configuration shows how to configure the factories: + +[source, java] +---- +@Autowired +private ConfigurationProperties props; + +@Bean +public CachingConnectionFactory 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 LocalizedQueueConnectionFactory queueAffinityCF( + @Qualifier("defaultConnectionFactory") ConnectionFactory defaultCF) { + return new LocalizedQueueConnectionFactory(defaultCF, + StringUtils.commaDelimitedListToStringArray(this.props.getAddresses()), + StringUtils.commaDelimitedListToStringArray(this.props.getAdminUris()), + StringUtils.commaDelimitedListToStringArray(this.props.getNodes()), + this.props.getVirtualHost(), this.props.getUsername(), this.props.getPassword(), + false, null); +} +---- + +Notice that the first three parameters are arrays of `addresses`, `adminUris`, and `nodes`. +These are positional in that, when a container attempts to connect to a queue, it uses the admin API to determine which node is the lead for the queue and connects to the address in the same array position as that node. + +IMPORTANT: Starting with version 3.0, the RabbitMQ `http-client` is no longer used to access the Rest API. +Instead, by default, the `WebClient` from Spring Webflux is used if `spring-webflux` is on the class path; otherwise a `RestTemplate` is used. + +To add `WebFlux` to the class path: + +.Maven +[source,xml,subs="+attributes"] +---- + + org.springframework.amqp + spring-rabbit + +---- +.Gradle +[source,groovy,subs="+attributes"] +---- +compile 'org.springframework.amqp:spring-rabbit' +---- + +You can also use other REST technology by implementing `LocalizedQueueConnectionFactory.NodeLocator` and overriding its `createClient, ``restCall`, and optionally, `close` methods. + +[source, java] +---- +lqcf.setNodeLocator(new NodeLocator() { + + @Override + public MyClient createClient(String userName, String password) { + ... + } + + @Override + public HashMap restCall(MyClient client, URI uri) { + ... + }); + +}); +---- + +The framework provides the `WebFluxNodeLocator` and `RestTemplateNodeLocator`, with the default as discussed above. + +[[cf-pub-conf-ret]] +== Publisher Confirms and Returns + +Confirmed (with correlation) and returned messages are supported by setting the `CachingConnectionFactory` property `publisherConfirmType` to `ConfirmType.CORRELATED` and the `publisherReturns` property to 'true'. + +When these options are set, `Channel` instances created by the factory are wrapped in an `PublisherCallbackChannel`, which is used to facilitate the callbacks. +When such a channel is obtained, the client can register a `PublisherCallbackChannel.Listener` with the `Channel`. +The `PublisherCallbackChannel` implementation contains logic to route a confirm or return to the appropriate listener. +These features are explained further in the following sections. + +See also xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns] and `simplePublisherConfirms` in xref:amqp/template.adoc#scoped-operations[Scoped Operations]. + +TIP: For some more background information, see the blog post by the RabbitMQ team titled https://www.rabbitmq.com/blog/2011/02/10/introducing-publisher-confirms/[Introducing Publisher Confirms]. + +[[connection-channel-listeners]] +== Connection and Channel Listeners + +The connection factory supports registering `ConnectionListener` and `ChannelListener` implementations. +This allows you to receive notifications for connection and channel related events. +(A `ConnectionListener` is used by the `RabbitAdmin` to perform declarations when the connection is established - see xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc#automatic-declaration[Automatic Declaration of Exchanges, Queues, and Bindings] for more information). +The following listing shows the `ConnectionListener` interface definition: + +[source, java] +---- +@FunctionalInterface +public interface ConnectionListener { + + void onCreate(Connection connection); + + default void onClose(Connection connection) { + } + + default void onShutDown(ShutdownSignalException signal) { + } + +} +---- + +Starting with version 2.0, the `org.springframework.amqp.rabbit.connection.Connection` object can be supplied with `com.rabbitmq.client.BlockedListener` instances to be notified for connection blocked and unblocked events. +The following example shows the ChannelListener interface definition: + +[source, java] +---- +@FunctionalInterface +public interface ChannelListener { + + void onCreate(Channel channel, boolean transactional); + + default void onShutDown(ShutdownSignalException signal) { + } + +} +---- + +See xref:amqp/template.adoc#publishing-is-async[Publishing is Asynchronous -- How to Detect Successes and Failures] for one scenario where you might want to register a `ChannelListener`. + +[[channel-close-logging]] +== Logging Channel Close Events + +Version 1.5 introduced a mechanism to enable users to control logging levels. + +The `AbstractConnectionFactory` uses a default strategy to log channel closures as follows: + +* Normal channel closes (200 OK) are not logged. +* If a channel is closed due to a failed passive queue declaration, it is logged at DEBUG level. +* If a channel is closed because the `basic.consume` is refused due to an exclusive consumer condition, it is logged at +DEBUG level (since 3.1, previously INFO). +* All others are logged at ERROR level. + +To modify this behavior, you can inject a custom `ConditionalExceptionLogger` into the +`CachingConnectionFactory` in its `closeExceptionLogger` property. + +Also, the `AbstractConnectionFactory.DefaultChannelCloseLogger` is now public, allowing it to be sub classed. + +See also xref:amqp/receiving-messages/consumer-events.adoc[Consumer Events]. + +[[runtime-cache-properties]] +== Runtime Cache Properties + +Staring with version 1.6, the `CachingConnectionFactory` now provides cache statistics through the `getCacheProperties()` +method. +These statistics can be used to tune the cache to optimize it in production. +For example, the high water marks can be used to determine whether the cache size should be increased. +If it equals the cache size, you might want to consider increasing further. +The following table describes the `CacheMode.CHANNEL` properties: + +.Cache properties for CacheMode.CHANNEL +[cols="2l,4", options="header"] +|=== +|Property + +|Meaning + +|connectionName + +|The name of the connection generated by the `ConnectionNameStrategy`. + +|channelCacheSize + +|The currently configured maximum channels that are allowed to be idle. + +|localPort + +|The local port for the connection (if available). +This can be used to correlate with connections and channels on the RabbitMQ Admin UI. + +|idleChannelsTx + +|The number of transactional channels that are currently idle (cached). + +|idleChannelsNotTx + +|The number of non-transactional channels that are currently idle (cached). + +|idleChannelsTxHighWater + +|The maximum number of transactional channels that have been concurrently idle (cached). + +|idleChannelsNotTxHighWater + +|The maximum number of non-transactional channels have been concurrently idle (cached). + +|=== + +The following table describes the `CacheMode.CONNECTION` properties: + +.Cache properties for CacheMode.CONNECTION +[cols="2l,4", options="header"] +|=== +|Property + +|Meaning + +|connectionName: + +|The name of the connection generated by the `ConnectionNameStrategy`. + +|openConnections + +|The number of connection objects representing connections to brokers. + +|channelCacheSize + +|The currently configured maximum channels that are allowed to be idle. + +|connectionCacheSize + +|The currently configured maximum connections that are allowed to be idle. + +|idleConnections + +|The number of connections that are currently idle. + +|idleConnectionsHighWater + +|The maximum number of connections that have been concurrently idle. + +|idleChannelsTx: + +|The number of transactional channels that are currently idle (cached) for this connection. +You can use the `localPort` part of the property name to correlate with connections and channels on the RabbitMQ Admin UI. + +|idleChannelsNotTx: + +|The number of non-transactional channels that are currently idle (cached) for this connection. +The `localPort` part of the property name can be used to correlate with connections and channels on the RabbitMQ Admin UI. + +|idleChannelsTxHighWater: + +|The maximum number of transactional channels that have been concurrently idle (cached). +The localPort part of the property name can be used to correlate with connections and channels on the RabbitMQ Admin UI. + +|idleChannelsNotTxHighWater: + +|The maximum number of non-transactional channels have been concurrently idle (cached). +You can use the `localPort` part of the property name to correlate with connections and channels on the RabbitMQ Admin UI. + +|=== + +The `cacheMode` property (`CHANNEL` or `CONNECTION`) is also included. + +.JVisualVM Example +image::cacheStats.png[align="center"] + +[[auto-recovery]] +== RabbitMQ Automatic Connection/Topology recovery + +Since the first version of Spring AMQP, the framework has provided its own connection and channel recovery in the event of a broker failure. +Also, as discussed in xref:amqp/broker-configuration.adoc[Configuring the Broker], the `RabbitAdmin` re-declares any infrastructure beans (queues and others) when the connection is re-established. +It therefore does not rely on the https://www.rabbitmq.com/api-guide.html#recovery[auto-recovery] that is now provided by the `amqp-client` library. +The `amqp-client`, has auto recovery enabled by default. +There are some incompatibilities between the two recovery mechanisms so, by default, Spring sets the `automaticRecoveryEnabled` property on the underlying `RabbitMQ connectionFactory` to `false`. +Even if the property is `true`, Spring effectively disables it, by immediately closing any recovered connections. + +IMPORTANT: By default, only elements (queues, exchanges, bindings) that are defined as beans will be re-declared after a connection failure. +See xref:amqp/broker-configuration.adoc#declarable-recovery[Recovering Auto-Delete Declarations] for how to change that behavior. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/containerAttributes.adoc b/src/reference/antora/modules/ROOT/pages/amqp/containerAttributes.adoc new file mode 100644 index 0000000000..5d63f226d0 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/containerAttributes.adoc @@ -0,0 +1,734 @@ +[[containerAttributes]] += Message Listener Container Configuration + +There are quite a few options for configuring a `SimpleMessageListenerContainer` (SMLC) and a `DirectMessageListenerContainer` (DMLC) related to transactions and quality of service, and some of them interact with each other. +Properties that apply to the SMLC, DMLC, or `StreamListenerContainer` (StLC) (see xref:stream.adoc[Using the RabbitMQ Stream Plugin]) are indicated by the check mark in the appropriate column. +See xref:amqp/receiving-messages/choose-container.adoc[Choosing a Container] for information to help you decide which container is appropriate for your application. + +The following table shows the container property names and their equivalent attribute names (in parentheses) when using the namespace to configure a ``. +The `type` attribute on that element can be `simple` (default) or `direct` to specify an `SMLC` or `DMLC` respectively. +Some properties are not exposed by the namespace. +These are indicated by `N/A` for the attribute. + +.Configuration options for a message listener container +[cols="8,16,1,1,1", options="header"] +|=== +|Property +(Attribute) +|Description +|SMLC +|DMLC +|StLC + +|[[ackTimeout]]<> + +(N/A) + +|When `messagesPerAck` is set, this timeout is used as an alternative to send an ack. +When a new message arrives, the count of unacked messages is compared to `messagesPerAck`, and the time since the last ack is compared to this value. +If either condition is `true`, the message is acknowledged. +When no new messages arrive and there are unacked messages, this timeout is approximate since the condition is only checked each `monitorInterval`. +See also `messagesPerAck` and `monitorInterval` in this table. + +a| +a|image::tickmark.png[] +a| + +|[[acknowledgeMode]]<> + +(acknowledge) + +a| +* `NONE`: No acks are sent (incompatible with `channelTransacted=true`). +RabbitMQ calls this "`autoack`", because the broker assumes all messages are acked without any action from the consumer. +* `MANUAL`: The listener must acknowledge all messages by calling `Channel.basicAck()`. +* `AUTO`: The container acknowledges the message automatically, unless the `MessageListener` throws an exception. +Note that `acknowledgeMode` is complementary to `channelTransacted` -- if the channel is transacted, the broker requires a commit notification in addition to the ack. +This is the default mode. +See also `batchSize`. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[adviceChain]]<> + +(advice-chain) + +|An array of AOP Advice to apply to the listener execution. +This can be used to apply additional cross-cutting concerns, such as automatic retry in the event of broker death. +Note that simple re-connection after an AMQP error is handled by the `CachingConnectionFactory`, as long as the broker is still alive. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[afterReceivePostProcessors]]<> + +(N/A) + +|An array of `MessagePostProcessor` instances that are invoked before invoking the listener. +Post processors can implement `PriorityOrdered` or `Ordered`. +The array is sorted with un-ordered members invoked last. +If a post processor returns `null`, the message is discarded (and acknowledged, if appropriate). + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[alwaysRequeueWithTxManagerRollback]]<> + +(N/A) + +|Set to `true` to always requeue messages on rollback when a transaction manager is configured. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[autoDeclare]]<> + +(auto-declare) + +a|When set to `true` (default), the container uses a `RabbitAdmin` to redeclare all AMQP objects (queues, exchanges, bindings), if it detects that at least one of its queues is missing during startup, perhaps because it is an `auto-delete` or an expired queue, but the redeclaration proceeds if the queue is missing for any reason. +To disable this behavior, set this property to `false`. +Note that the container fails to start if all of its queues are missing. + +NOTE: Prior to version 1.6, if there was more than one admin in the context, the container would randomly select one. +If there were no admins, it would create one internally. +In either case, this could cause unexpected results. +Starting with version 1.6, for `autoDeclare` to work, there must be exactly one `RabbitAdmin` in the context, or a reference to a specific instance must be configured on the container using the `rabbitAdmin` property. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[autoStartup]]<> + +(auto-startup) + +|Flag to indicate that the container should start when the `ApplicationContext` does (as part of the `SmartLifecycle` callbacks, which happen after all beans are initialized). +Defaults to `true`, but you can set it to `false` if your broker might not be available on startup and call `start()` later manually when you know the broker is ready. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a|image::tickmark.png[] + +|[[batchSize]]<> + +(transaction-size) +(batch-size) + +|When used with `acknowledgeMode` set to `AUTO`, the container tries to process up to this number of messages before sending an ack (waiting for each one up to the receive timeout setting). +This is also when a transactional channel is committed. +If the `prefetchCount` is less than the `batchSize`, it is increased to match the `batchSize`. + +a|image::tickmark.png[] +a| +a| + +|[[batchingStrategy]]<> + +(N/A) + +|The strategy used when debatchng messages. +Default `SimpleDebatchingStrategy`. +See xref:amqp/sending-messages.adoc#template-batching[Batching] and xref:amqp/receiving-messages/batch.adoc[@RabbitListener with Batching]. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[channelTransacted]]<> + +(channel-transacted) + +|Boolean flag to signal that all messages should be acknowledged in a transaction (either manually or automatically). + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[concurrency]]<> + +(N/A) + +|`m-n` The range of concurrent consumers for each listener (min, max). +If only `n` is provided, `n` is a fixed number of consumers. +See <>. + +a|image::tickmark.png[] +a| +a| + +|[[concurrentConsumers]]<> + +(concurrency) + +|The number of concurrent consumers to initially start for each listener. +See <>. +For the `StLC`, concurrency is controlled via an overloaded `superStream` method; see xref:stream.adoc#super-stream-consumer[Consuming Super Streams with Single Active Consumers]. + +a|image::tickmark.png[] +a| +a|image::tickmark.png[] + +|[[connectionFactory]]<> + +(connection-factory) + +|A reference to the `ConnectionFactory`. +When configuring by using the XML namespace, the default referenced bean name is `rabbitConnectionFactory`. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[consecutiveActiveTrigger]]<> + +(min-consecutive-active) + +|The minimum number of consecutive messages received by a consumer, without a receive timeout occurring, when considering starting a new consumer. +Also impacted by 'batchSize'. +See <>. +Default: 10. + +a|image::tickmark.png[] +a| +a| + +|[[consecutiveIdleTrigger]]<> + +(min-consecutive-idle) + +|The minimum number of receive timeouts a consumer must experience before considering stopping a consumer. +Also impacted by 'batchSize'. +See <>. +Default: 10. + +a|image::tickmark.png[] +a| +a| + +|[[consumerBatchEnabled]]<> + +(batch-enabled) + +|If the `MessageListener` supports it, setting this to true enables batching of discrete messages, up to `batchSize`; a partial batch will be delivered if no new messages arrive in `receiveTimeout`. +When this is false, batching is only supported for batches created by a producer; see xref:amqp/sending-messages.adoc#template-batching[Batching]. + +a|image::tickmark.png[] +a| +a| + +|[[consumerCustomizer]]<> + +(N/A) + +|A `ConsumerCustomizer` bean used to modify stream consumers created by the container. + +a| +a| +a|image::tickmark.png[] + +|[[consumerStartTimeout]]<> + +(N/A) + +|The time in milliseconds to wait for a consumer thread to start. +If this time elapses, an error log is written. +An example of when this might happen is if a configured `taskExecutor` has insufficient threads to support the container `concurrentConsumers`. + +See xref:amqp/receiving-messages/threading.adoc[Threading and Asynchronous Consumers]. +Default: 60000 (one minute). + +a|image::tickmark.png[] +a| +a| + +|[[consumerTagStrategy]]<> + +(consumer-tag-strategy) + +|Set an implementation of xref:amqp/receiving-messages/consumerTags.adoc[ConsumerTagStrategy], enabling the creation of a (unique) tag for each consumer. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[consumersPerQueue]]<> + +(consumers-per-queue) + +|The number of consumers to create for each configured queue. +See <>. + +a| +a|image::tickmark.png[] +a| + +|[[consumeDelay]]<> + +(N/A) + +|When using the https://github.com/rabbitmq/rabbitmq-sharding[RabbitMQ Sharding Plugin] with `concurrentConsumers > 1`, there is a race condition that can prevent even distribution of the consumers across the shards. +Use this property to add a small delay between consumer starts to avoid this race condition. +You should experiment with values to determine the suitable delay for your environment. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[debatchingEnabled]]<> + +(N/A) + +|When true, the listener container will debatch batched messages and invoke the listener with each message from the batch. +Starting with version 2.2.7, xref:amqp/sending-messages.adoc#template-batching[producer created batches] will be debatched as a `List` if the listener is a `BatchMessageListener` or `ChannelAwareBatchMessageListener`. +Otherwise messages from the batch are presented one-at-a-time. +Default true. +See xref:amqp/sending-messages.adoc#template-batching[Batching] and xref:amqp/receiving-messages/batch.adoc[@RabbitListener with Batching]. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[declarationRetries]]<> + +(declaration-retries) + +|The number of retry attempts when passive queue declaration fails. +Passive queue declaration occurs when the consumer starts or, when consuming from multiple queues, when not all queues were available during initialization. +When none of the configured queues can be passively declared (for any reason) after the retries are exhausted, the container behavior is controlled by the 'missingQueuesFatal` property, described earlier. +Default: Three retries (for a total of four attempts). + +a|image::tickmark.png[] +a| +a| + +|[[defaultRequeueRejected]]<> + +(requeue-rejected) + +|Determines whether messages that are rejected because the listener threw an exception should be requeued or not. +Default: `true`. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[errorHandler]]<> + +(error-handler) + +|A reference to an `ErrorHandler` strategy for handling any uncaught exceptions that may occur during the execution of the MessageListener. +Default: `ConditionalRejectingErrorHandler` + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[exclusive]]<> + +(exclusive) + +|Determines whether the single consumer in this container has exclusive access to the queues. +The concurrency of the container must be 1 when this is `true`. +If another consumer has exclusive access, the container tries to recover the consumer, according to the +`recovery-interval` or `recovery-back-off`. +When using the namespace, this attribute appears on the `` element along with the queue names. +Default: `false`. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[exclusiveConsumerExceptionLogger]]<> + +(N/A) + +|An exception logger used when an exclusive consumer cannot gain access to a queue. +By default, this is logged at the `WARN` level. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[failedDeclarationRetryInterval]]<> + +(failed-declaration +-retry-interval) + +|The interval between passive queue declaration retry attempts. +Passive queue declaration occurs when the consumer starts or, when consuming from multiple queues, when not all queues were available during initialization. +Default: 5000 (five seconds). + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[forceCloseChannel]]<> + +(N/A) + +|If the consumers do not respond to a shutdown within `shutdownTimeout`, if this is `true`, the channel will be closed, causing any unacked messages to be requeued. +Defaults to `true` since 2.0. +You can set it to `false` to revert to the previous behavior. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[forceStop]]<> + +(N/A) + +|Set to true to stop (when the container is stopped) after the current record is processed; causing all prefetched messages to be requeued. +By default, the container will cancel the consumer and process all prefetched messages before stopping. +Since versions 2.4.14, 3.0.6 +Defaults to `false`. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[globalQos]]<> + +(global-qos) + +|When true, the `prefetchCount` is applied globally to the channel rather than to each consumer on the channel. +See https://www.rabbitmq.com/amqp-0-9-1-reference.html#basic.qos.global[`basicQos.global`] for more information. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|(group) + +|This is available only when using the namespace. +When specified, a bean of type `Collection` is registered with this name, and the +container for each `` element is added to the collection. +This allows, for example, starting and stopping the group of containers by iterating over the collection. +If multiple `` elements have the same group value, the containers in the collection form +an aggregate of all containers so designated. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[idleEventInterval]]<> + +(idle-event-interval) + +|See xref:amqp/receiving-messages/idle-containers.adoc[Detecting Idle Asynchronous Consumers]. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[javaLangErrorHandler]]<> + +(N/A) + +|An `AbstractMessageListenerContainer.JavaLangErrorHandler` implementation that is called when a container thread catches an `Error`. +The default implementation calls `System.exit(99)`; to revert to the previous behavior (do nothing), add a no-op handler. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[maxConcurrentConsumers]]<> + +(max-concurrency) + +|The maximum number of concurrent consumers to start, if needed, on demand. +Must be greater than or equal to 'concurrentConsumers'. +See <>. + +a|image::tickmark.png[] +a| +a| + +|[[messagesPerAck]]<> + +(N/A) + +|The number of messages to receive between acks. +Use this to reduce the number of acks sent to the broker (at the cost of increasing the possibility of redelivered messages). +Generally, you should set this property only on high-volume listener containers. +If this is set and a message is rejected (exception thrown), pending acks are acknowledged and the failed message is rejected. +Not allowed with transacted channels. +If the `prefetchCount` is less than the `messagesPerAck`, it is increased to match the `messagesPerAck`. +Default: ack every message. +See also `ackTimeout` in this table. + +a| +a|image::tickmark.png[] +a| + +|[[mismatchedQueuesFatal]]<> + +(mismatched-queues-fatal) + +a|When the container starts, if this property is `true` (default: `false`), the container checks that all queues declared in the context are compatible with queues already on the broker. +If mismatched properties (such as `auto-delete`) or arguments (skuch as `x-message-ttl`) exist, the container (and application context) fails to start with a fatal exception. + +If the problem is detected during recovery (for example, after a lost connection), the container is stopped. + +There must be a single `RabbitAdmin` in the application context (or one specifically configured on the container by using the `rabbitAdmin` property). +Otherwise, this property must be `false`. + +NOTE: If the broker is not available during initial startup, the container starts and the conditions are checked when the connection is established. + +IMPORTANT: The check is done against all queues in the context, not just the queues that a particular listener is configured to use. +If you wish to limit the checks to just those queues used by a container, you should configure a separate `RabbitAdmin` for the container, and provide a reference to it using the `rabbitAdmin` property. +See xref:amqp/broker-configuration.adoc#conditional-declaration[Conditional Declaration] for more information. + +IMPORTANT: Mismatched queue argument detection is disabled while starting a container for a `@RabbitListener` in a bean that is marked `@Lazy`. +This is to avoid a potential deadlock which can delay the start of such containers for up to 60 seconds. +Applications using lazy listener beans should check the queue arguments before getting a reference to the lazy bean. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[missingQueuesFatal]]<> + +(missing-queues-fatal) + +a|When set to `true` (default), if none of the configured queues are available on the broker, it is considered fatal. +This causes the application context to fail to initialize during startup. +Also, when the queues are deleted while the container is running, by default, the consumers make three retries to connect to the queues (at five second intervals) and stop the container if these attempts fail. + +This was not configurable in previous versions. + +When set to `false`, after making the three retries, the container goes into recovery mode, as with other problems, such as the broker being down. +The container tries to recover according to the `recoveryInterval` property. +During each recovery attempt, each consumer again tries four times to passively declare the queues at five second intervals. +This process continues indefinitely. + +You can also use a properties bean to set the property globally for all containers, as follows: + +[source,xml] +---- + + + false + + +---- + +This global property is not applied to any containers that have an explicit `missingQueuesFatal` property set. + +The default retry properties (three retries at five-second intervals) can be overridden by setting the properties below. + +IMPORTANT: Missing queue detection is disabled while starting a container for a `@RabbitListener` in a bean that is marked `@Lazy`. +This is to avoid a potential deadlock which can delay the start of such containers for up to 60 seconds. +Applications using lazy listener beans should check the queue(s) before getting a reference to the lazy bean. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[monitorInterval]]<> + +(monitor-interval) + +|With the DMLC, a task is scheduled to run at this interval to monitor the state of the consumers and recover any that have failed. + +a| +a|image::tickmark.png[] +a| + +|[[noLocal]]<> + +(N/A) + +|Set to `true` to disable delivery from the server to consumers messages published on the same channel's connection. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[phase]]<> + +(phase) + +|When `autoStartup` is `true`, the lifecycle phase within which this container should start and stop. +The lower the value, the earlier this container starts and the later it stops. +The default is `Integer.MAX_VALUE`, meaning the container starts as late as possible and stops as soon as possible. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[possibleAuthenticationFailureFatal]]<> + +(possible-authentication-failure-fatal) + +a|When set to `true` (default for SMLC), if a `PossibleAuthenticationFailureException` is thrown during connection, it is considered fatal. +This causes the application context to fail to initialize during startup (if the container is configured with auto startup). + +Since _version 2.0_. + +**DirectMessageListenerContainer** + +When set to `false` (default), each consumer will attempt to reconnect according to the `monitorInterval`. + +**SimpleMessageListenerContainer** + +When set to `false`, after making the 3 retries, the container will go into recovery mode, as with other problems, such as the broker being down. +The container will attempt to recover according to the `recoveryInterval` property. +During each recovery attempt, each consumer will again try 4 times to start. +This process will continue indefinitely. + +You can also use a properties bean to set the property globally for all containers, as follows: + +[source,xml] +---- + + + false + + +---- + +This global property will not be applied to any containers that have an explicit `missingQueuesFatal` property set. + +The default retry properties (3 retries at 5 second intervals) can be overridden using the properties after this one. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[prefetchCount]]<> + +(prefetch) + +a|The number of unacknowledged messages that can be outstanding at each consumer. +The higher this value is, the faster the messages can be delivered, but the higher the risk of non-sequential processing. +Ignored if the `acknowledgeMode` is `NONE`. +This is increased, if necessary, to match the `batchSize` or `messagePerAck`. +Defaults to 250 since 2.0. +You can set it to 1 to revert to the previous behavior. + +IMPORTANT: There are scenarios where the prefetch value should +be low -- for example, with large messages, especially if the processing is slow (messages could add up +to a large amount of memory in the client process), and if strict message ordering is necessary +(the prefetch value should be set back to 1 in this case). +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. + +Also see `globalQos`. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[rabbitAdmin]]<> + +(admin) + +|When a listener container listens to at least one auto-delete queue and it is found to be missing during startup, the container uses a `RabbitAdmin` to declare the queue and any related bindings and exchanges. +If such elements are configured to use conditional declaration (see xref:amqp/broker-configuration.adoc#conditional-declaration[Conditional Declaration]), the container must use the admin that was configured to declare those elements. +Specify that admin here. +It is required only when using auto-delete queues with conditional declaration. +If you do not wish the auto-delete queues to be declared until the container is started, set `auto-startup` to `false` on the admin. +Defaults to a `RabbitAdmin` that declares all non-conditional elements. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[receiveTimeout]]<> + +(receive-timeout) + +|The maximum time to wait for each message. +If `acknowledgeMode=NONE`, this has very little effect -- the container spins round and asks for another message. +It has the biggest effect for a transactional `Channel` with `batchSize > 1`, since it can cause messages already consumed not to be acknowledged until the timeout expires. +When `consumerBatchEnabled` is true, a partial batch will be delivered if this timeout occurs before a batch is complete. + +a|image::tickmark.png[] +a| +a| + +|[[recoveryBackOff]]<> + +(recovery-back-off) + +|Specifies the `BackOff` for intervals between attempts to start a consumer if it fails to start for non-fatal reasons. +Default is `FixedBackOff` with unlimited retries every five seconds. +Mutually exclusive with `recoveryInterval`. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[recoveryInterval]]<> + +(recovery-interval) + +|Determines the time in milliseconds between attempts to start a consumer if it fails to start for non-fatal reasons. +Default: 5000. +Mutually exclusive with `recoveryBackOff`. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[retryDeclarationInterval]]<> + +(missing-queue- +retry-interval) + +|If a subset of the configured queues are available during consumer initialization, the consumer starts consuming from those queues. +The consumer tries to passively declare the missing queues by using this interval. +When this interval elapses, the 'declarationRetries' and 'failedDeclarationRetryInterval' is used again. +If there are still missing queues, the consumer again waits for this interval before trying again. +This process continues indefinitely until all queues are available. +Default: 60000 (one minute). + +a|image::tickmark.png[] +a| +a| + +|[[shutdownTimeout]]<> + +(N/A) + +|When a container shuts down (for example, +if its enclosing `ApplicationContext` is closed), it waits for in-flight messages to be processed up to this limit. +Defaults to five seconds. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[startConsumerMinInterval]]<> + +(min-start-interval) + +|The time in milliseconds that must elapse before each new consumer is started on demand. +See <>. +Default: 10000 (10 seconds). + +a|image::tickmark.png[] +a| +a| + +|[[statefulRetryFatal]]<> + +WithNullMessageId +(N/A) + +|When using a stateful retry advice, if a message with a missing `messageId` property is received, it is considered +fatal for the consumer (it is stopped) by default. +Set this to `false` to discard (or route to a dead-letter queue) such messages. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[stopConsumerMinInterval]]<> + +(min-stop-interval) + +|The time in milliseconds that must elapse before a consumer is stopped since the last consumer was stopped when an idle consumer is detected. +See <>. +Default: 60000 (one minute). + +a|image::tickmark.png[] +a| +a| + +|[[streamConverter]]<> + +(N/A) + +|A `StreamMessageConverter` to convert a native Stream message to a Spring AMQP message. + +a| +a| +a|image::tickmark.png[] + +|[[taskExecutor]]<> + +(task-executor) + +|A reference to a Spring `TaskExecutor` (or standard JDK 1.5+ `Executor`) for executing listener invokers. +Default is a `SimpleAsyncTaskExecutor`, using internally managed threads. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| + +|[[taskScheduler]]<> + +(task-scheduler) + +|With the DMLC, the scheduler used to run the monitor task at the 'monitorInterval'. + +a| +a|image::tickmark.png[] +a| + +|[[transactionManager]]<> + +(transaction-manager) + +|External transaction manager for the operation of the listener. +Also complementary to `channelTransacted` -- if the `Channel` is transacted, its transaction is synchronized with the external transaction. + +a|image::tickmark.png[] +a|image::tickmark.png[] +a| +|=== + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/containers-and-broker-named-queues.adoc b/src/reference/antora/modules/ROOT/pages/amqp/containers-and-broker-named-queues.adoc new file mode 100644 index 0000000000..4ca70426e2 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/containers-and-broker-named-queues.adoc @@ -0,0 +1,35 @@ +[[containers-and-broker-named-queues]] += Containers and Broker-Named queues + +While it is preferable to use `AnonymousQueue` instances as auto-delete queues, starting with version 2.1, you can use broker named queues with listener containers. +The following example shows how to do so: + +[source, java] +---- +@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. + +NOTE: You cannot add broker-named queues to the containers while they are running. + +IMPORTANT: 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. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/custom-client-props.adoc b/src/reference/antora/modules/ROOT/pages/amqp/custom-client-props.adoc new file mode 100644 index 0000000000..e80e4974bd --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/custom-client-props.adoc @@ -0,0 +1,15 @@ +[[custom-client-props]] += Adding Custom Client Connection Properties +:page-section-summary-toc: 1 + +The `CachingConnectionFactory` now lets you access the underlying connection factory to allow, for example, +setting custom client properties. +The following example shows how to do so: + +[source, java] +---- +connectionFactory.getRabbitConnectionFactory().getClientProperties().put("thing1", "thing2"); +---- + +These properties appear in the RabbitMQ Admin UI when viewing the connection. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/debugging.adoc b/src/reference/antora/modules/ROOT/pages/amqp/debugging.adoc new file mode 100644 index 0000000000..5ea5ccef21 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/debugging.adoc @@ -0,0 +1,12 @@ +[[debugging]] += Debugging +:page-section-summary-toc: 1 + +Spring AMQP provides extensive logging, especially at the `DEBUG` level. + +If you wish to monitor the AMQP protocol between the application and broker, you can use a tool such as WireShark, which has a plugin to decode the protocol. +Alternatively, the RabbitMQ Java client comes with a very useful class called `Tracer`. +When run as a `main`, by default, it listens on port 5673 and connects to port 5672 on localhost. +You can run it and change your connection factory configuration to connect to port 5673 on localhost. +It displays the decoded protocol on the console. +Refer to the `Tracer` Javadoc for more information. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/delayed-message-exchange.adoc b/src/reference/antora/modules/ROOT/pages/amqp/delayed-message-exchange.adoc new file mode 100644 index 0000000000..5593e3b475 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/delayed-message-exchange.adoc @@ -0,0 +1,51 @@ +[[delayed-message-exchange]] += Delayed Message Exchange + +Version 1.6 introduces support for the +https://www.rabbitmq.com/blog/2015/04/16/scheduling-messages-with-rabbitmq/[Delayed Message Exchange Plugin] + +NOTE: The plugin is currently marked as experimental but has been available for over a year (at the time of writing). +If changes to the plugin make it necessary, we plan to add support for such changes as soon as practical. +For that reason, this support in Spring AMQP should be considered experimental, too. +This functionality was tested with RabbitMQ 3.6.0 and version 0.0.1 of the plugin. + +To use a `RabbitAdmin` to declare an exchange as delayed, you can set the `delayed` property on the exchange bean to +`true`. +The `RabbitAdmin` uses the exchange type (`Direct`, `Fanout`, and so on) to set the `x-delayed-type` argument and +declare the exchange with type `x-delayed-message`. + +The `delayed` property (default: `false`) is also available when configuring exchange beans using XML. +The following example shows how to use it: + +[source, xml] +---- + +---- + +To send a delayed message, you can set the `x-delay` header through `MessageProperties`, as the following examples show: + +[source, java] +---- +MessageProperties properties = new MessageProperties(); +properties.setDelay(15000); +template.send(exchange, routingKey, + MessageBuilder.withBody("foo".getBytes()).andProperties(properties).build()); +---- + +[source, java] +---- +rabbitTemplate.convertAndSend(exchange, routingKey, "foo", new MessagePostProcessor() { + + @Override + public Message postProcessMessage(Message message) throws AmqpException { + message.getMessageProperties().setDelay(15000); + return message; + } + +}); +---- + +To check if a message was delayed, use the `getReceivedDelay()` method on the `MessageProperties`. +It is a separate property to avoid unintended propagation to an output message generated from an input message. + + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/exception-handling.adoc b/src/reference/antora/modules/ROOT/pages/amqp/exception-handling.adoc new file mode 100644 index 0000000000..9658b72b57 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/exception-handling.adoc @@ -0,0 +1,46 @@ +[[exception-handling]] += Exception Handling + +Many operations with the RabbitMQ Java client can throw checked exceptions. +For example, there are a lot of cases where `IOException` instances may be thrown. +The `RabbitTemplate`, `SimpleMessageListenerContainer`, and other Spring AMQP components catch those exceptions and convert them into one of the exceptions within `AmqpException` hierarchy. +Those are defined in the 'org.springframework.amqp' package, and `AmqpException` is the base of the hierarchy. + +When a listener throws an exception, it is wrapped in a `ListenerExecutionFailedException`. +Normally the message is rejected and requeued by the broker. +Setting `defaultRequeueRejected` to `false` causes messages to be discarded (or routed to a dead letter exchange). +As discussed in xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc#async-listeners[Message Listeners and the Asynchronous Case], the listener can throw an `AmqpRejectAndDontRequeueException` (or `ImmediateRequeueAmqpException`) to conditionally control this behavior. + +However, there is a class of errors where the listener cannot control the behavior. +When a message that cannot be converted is encountered (for example, an invalid `content_encoding` header), some exceptions are thrown before the message reaches user code. +With `defaultRequeueRejected` set to `true` (default) (or throwing an `ImmediateRequeueAmqpException`), such messages would be redelivered over and over. +Before version 1.3.2, users needed to write a custom `ErrorHandler`, as discussed in xref:amqp/exception-handling.adoc[Exception Handling], to avoid this situation. + +Starting with version 1.3.2, the default `ErrorHandler` is now a `ConditionalRejectingErrorHandler` that rejects (and does not requeue) messages that fail with an irrecoverable error. +Specifically, it rejects messages that fail with the following errors: + +* `o.s.amqp...MessageConversionException`: Can be thrown when converting the incoming message payload using a `MessageConverter`. +* `o.s.messaging...MessageConversionException`: Can be thrown by the conversion service if additional conversion is required when mapping to a `@RabbitListener` method. +* `o.s.messaging...MethodArgumentNotValidException`: Can be thrown if validation (for example, `@Valid`) is used in the listener and the validation fails. +* `o.s.messaging...MethodArgumentTypeMismatchException`: Can be thrown if the inbound message was converted to a type that is not correct for the target method. +For example, the parameter is declared as `Message` but `Message` is received. +* `java.lang.NoSuchMethodException`: Added in version 1.6.3. +* `java.lang.ClassCastException`: Added in version 1.6.3. + +You can configure an instance of this error handler with a `FatalExceptionStrategy` so that users can provide their own rules for conditional message rejection -- for example, a delegate implementation to the `BinaryExceptionClassifier` from Spring Retry (xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc#async-listeners[Message Listeners and the Asynchronous Case]). +In addition, the `ListenerExecutionFailedException` now has a `failedMessage` property that you can use in the decision. +If the `FatalExceptionStrategy.isFatal()` method returns `true`, the error handler throws an `AmqpRejectAndDontRequeueException`. +The default `FatalExceptionStrategy` logs a warning message when an exception is determined to be fatal. + +Since version 1.6.3, a convenient way to add user exceptions to the fatal list is to subclass `ConditionalRejectingErrorHandler.DefaultExceptionStrategy` and override the `isUserCauseFatal(Throwable cause)` method to return `true` for fatal exceptions. + +A common pattern for handling DLQ messages is to set a `time-to-live` on those messages as well as additional DLQ configuration such that these messages expire and are routed back to the main queue for retry. +The problem with this technique is that messages that cause fatal exceptions loop forever. +Starting with version 2.1, the `ConditionalRejectingErrorHandler` detects an `x-death` header on a message that causes a fatal exception to be thrown. +The message is logged and discarded. +You can revert to the previous behavior by setting the `discardFatalsWithXDeath` property on the `ConditionalRejectingErrorHandler` to `false`. + +IMPORTANT: Starting with version 2.1.9, messages with these fatal exceptions are rejected and NOT requeued by default, even if the container acknowledge mode is MANUAL. +These exceptions generally occur before the listener is invoked so the listener does not have a chance to ack or nack the message so it remained in the queue in an un-acked state. +To revert to the previous behavior, set the `rejectManual` property on the `ConditionalRejectingErrorHandler` to `false`. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/exclusive-consumer.adoc b/src/reference/antora/modules/ROOT/pages/amqp/exclusive-consumer.adoc new file mode 100644 index 0000000000..23c1a36a39 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/exclusive-consumer.adoc @@ -0,0 +1,10 @@ +[[exclusive-consumer]] += Exclusive Consumer +:page-section-summary-toc: 1 + +Starting with version 1.3, you can configure the listener container with a single exclusive consumer. +This prevents other containers from consuming from the queues until the current consumer is cancelled. +The concurrency of such a container must be `1`. + +When using exclusive consumers, other containers try to consume from the queues according to the `recoveryInterval` property and log a `WARN` message if the attempt fails. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/listener-concurrency.adoc b/src/reference/antora/modules/ROOT/pages/amqp/listener-concurrency.adoc new file mode 100644 index 0000000000..8a76771a52 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/listener-concurrency.adoc @@ -0,0 +1,46 @@ +[[listener-concurrency]] += Listener Concurrency + +[[simplemessagelistenercontainer]] +== SimpleMessageListenerContainer + +By default, the listener container starts a single consumer that receives messages from the queues. + +When examining the table in the previous section, you can see a number of properties and attributes that control concurrency. +The simplest is `concurrentConsumers`, which creates that (fixed) number of consumers that concurrently process messages. + +Prior to version 1.3.0, this was the only setting available and the container had to be stopped and started again to change the setting. + +Since version 1.3.0, you can now dynamically adjust the `concurrentConsumers` property. +If it is changed while the container is running, consumers are added or removed as necessary to adjust to the new setting. + +In addition, a new property called `maxConcurrentConsumers` has been added and the container dynamically adjusts the concurrency based on workload. +This works in conjunction with four additional properties: `consecutiveActiveTrigger`, `startConsumerMinInterval`, `consecutiveIdleTrigger`, and `stopConsumerMinInterval`. +With the default settings, the algorithm to increase consumers works as follows: + +If the `maxConcurrentConsumers` has not been reached and an existing consumer is active for ten consecutive cycles AND at least 10 seconds has elapsed since the last consumer was started, a new consumer is started. +A consumer is considered active if it received at least one message in `batchSize` * `receiveTimeout` milliseconds. + +With the default settings, the algorithm to decrease consumers works as follows: + +If there are more than `concurrentConsumers` running and a consumer detects ten consecutive timeouts (idle) AND the last consumer was stopped at least 60 seconds ago, a consumer is stopped. +The timeout depends on the `receiveTimeout` and the `batchSize` properties. +A consumer is considered idle if it receives no messages in `batchSize` * `receiveTimeout` milliseconds. +So, with the default timeout (one second) and a `batchSize` of four, stopping a consumer is considered after 40 seconds of idle time (four timeouts correspond to one idle detection). + +NOTE: Practically, consumers can be stopped only if the whole container is idle for some time. +This is because the broker shares its work across all the active consumers. + +Each consumer uses a single channel, regardless of the number of configured queues. + +Starting with version 2.0, the `concurrentConsumers` and `maxConcurrentConsumers` properties can be set with the `concurrency` property -- for example, `2-4`. + +[[using-directmessagelistenercontainer]] +== Using `DirectMessageListenerContainer` + +With this container, concurrency is based on the configured queues and `consumersPerQueue`. +Each consumer for each queue uses a separate channel, and the concurrency is controlled by the rabbit client library. +By default, at the time of writing, it uses a pool of `DEFAULT_NUM_THREADS = Runtime.getRuntime().availableProcessors() * 2` threads. + +You can configure a `taskExecutor` to provide the required maximum concurrency. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/listener-queues.adoc b/src/reference/antora/modules/ROOT/pages/amqp/listener-queues.adoc new file mode 100644 index 0000000000..1e52cef453 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/listener-queues.adoc @@ -0,0 +1,19 @@ +[[listener-queues]] += Listener Container Queues +:page-section-summary-toc: 1 + +Version 1.3 introduced a number of improvements for handling multiple queues in a listener container. + +Container can be initially configured to listen on zero queues. +Queues can be added and removed at runtime. +The `SimpleMessageListenerContainer` recycles (cancels and re-creates) all consumers when any pre-fetched messages have been processed. +The `DirectMessageListenerContainer` creates/cancels individual consumer(s) for each queue without affecting consumers on other queues. +See the https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.html[Javadoc] for the `addQueues`, `addQueueNames`, `removeQueues` and `removeQueueNames` methods. + +If not all queues are available, the container tries to passively declare (and consume from) the missing queues every 60 seconds. + +Also, if a consumer receives a cancel from the broker (for example, if a queue is deleted) the consumer tries to recover, and the recovered consumer continues to process messages from any other configured queues. +Previously, a cancel on one queue cancelled the entire consumer and, eventually, the container would stop due to the missing queue. + +If you wish to permanently remove a queue, you should update the container before or after deleting to queue, to avoid future attempts trying to consume from it. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/management-rest-api.adoc b/src/reference/antora/modules/ROOT/pages/amqp/management-rest-api.adoc new file mode 100644 index 0000000000..445b46c8a1 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/management-rest-api.adoc @@ -0,0 +1,14 @@ +[[management-rest-api]] += RabbitMQ REST API +:page-section-summary-toc: 1 + +When the management plugin is enabled, the RabbitMQ server exposes a REST API to monitor and configure the broker. +A https://github.com/rabbitmq/hop[Java Binding for the API] is now provided. +The `com.rabbitmq.http.client.Client` is a standard, immediate, and, therefore, blocking API. +It is based on the https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#spring-web[Spring Web] module and its `RestTemplate` implementation. +On the other hand, the `com.rabbitmq.http.client.ReactorNettyClient` is a reactive, non-blocking implementation based on the https://projectreactor.io/docs/netty/release/reference/docs/index.html[Reactor Netty] project. + +The hop dependency (`com.rabbitmq:http-client`) is now also `optional`. + +See their Javadoc for more information. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/message-converters.adoc b/src/reference/antora/modules/ROOT/pages/amqp/message-converters.adoc new file mode 100644 index 0000000000..b828342c4b --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/message-converters.adoc @@ -0,0 +1,513 @@ +[[message-converters]] += Message Converters + +The `AmqpTemplate` also defines several methods for sending and receiving messages that delegate to a `MessageConverter`. +The `MessageConverter` provides a single method for each direction: one for converting *to* a `Message` and another for converting *from* a `Message`. +Notice that, when converting to a `Message`, you can also provide properties in addition to the object. +The `object` parameter typically corresponds to the Message body. +The following listing shows the `MessageConverter` interface definition: + +[source,java] +---- +public interface MessageConverter { + + Message toMessage(Object object, MessageProperties messageProperties) + throws MessageConversionException; + + Object fromMessage(Message message) throws MessageConversionException; + +} +---- + +The relevant `Message`-sending methods on the `AmqpTemplate` are simpler than the methods we discussed previously, because they do not require the `Message` instance. +Instead, the `MessageConverter` is responsible for "`creating`" each `Message` by converting the provided object to the byte array for the `Message` body and then adding any provided `MessageProperties`. +The following listing shows the definitions of the various methods: + +[source,java] +---- +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; +---- + +On the receiving side, there are only two methods: one that accepts the queue name and one that relies on the template's "`queue`" property having been set. +The following listing shows the definitions of the two methods: + +[source,java] +---- +Object receiveAndConvert() throws AmqpException; + +Object receiveAndConvert(String queueName) throws AmqpException; +---- + +NOTE: The `MessageListenerAdapter` mentioned in xref:amqp/receiving-messages/async-consumer.adoc[Asynchronous Consumer] also uses a `MessageConverter`. + +[[simple-message-converter]] +== `SimpleMessageConverter` + +The default implementation of the `MessageConverter` strategy is called `SimpleMessageConverter`. +This is the converter that is used by an instance of `RabbitTemplate` if you do not explicitly configure an alternative. +It handles text-based content, serialized Java objects, and byte arrays. + +[[converting-from-a-message]] +=== Converting From a `Message` + +If the content type of the input `Message` begins with "text" (for example, +"text/plain"), it also checks for the content-encoding property to determine the charset to be used when converting the `Message` body byte array to a Java `String`. +If no content-encoding property had been set on the input `Message`, it uses the UTF-8 charset by default. +If you need to override that default setting, you can configure an instance of `SimpleMessageConverter`, set its `defaultCharset` property, and inject that into a `RabbitTemplate` instance. + +If the content-type property value of the input `Message` is set to "application/x-java-serialized-object", the `SimpleMessageConverter` tries to deserialize (rehydrate) the byte array into a Java object. +While that might be useful for simple prototyping, we do not recommend relying on Java serialization, since it leads to tight coupling between the producer and the consumer. +Of course, it also rules out usage of non-Java systems on either side. +With AMQP being a wire-level protocol, it would be unfortunate to lose much of that advantage with such restrictions. +In the next two sections, we explore some alternatives for passing rich domain object content without relying on Java serialization. + +For all other content-types, the `SimpleMessageConverter` returns the `Message` body content directly as a byte array. + +See <> for important information. + +[[converting-to-a-message]] +=== Converting To a `Message` + +When converting to a `Message` from an arbitrary Java Object, the `SimpleMessageConverter` likewise deals with byte arrays, strings, and serializable instances. +It converts each of these to bytes (in the case of byte arrays, there is nothing to convert), and it sets the content-type property accordingly. +If the `Object` to be converted does not match one of those types, the `Message` body is null. + +[[serializer-message-converter]] +== `SerializerMessageConverter` + +This converter is similar to the `SimpleMessageConverter` except that it can be configured with other Spring Framework +`Serializer` and `Deserializer` implementations for `application/x-java-serialized-object` conversions. + +See <> for important information. + +[[json-message-converter]] +== Jackson2JsonMessageConverter + +This section covers using the `Jackson2JsonMessageConverter` to convert to and from a `Message`. +It has the following sections: + +* xref:amqp/message-converters.adoc#Jackson2JsonMessageConverter-to-message[Converting to a `Message`] +* xref:amqp/message-converters.adoc#Jackson2JsonMessageConverter-from-message[Converting from a `Message`] + +[[Jackson2JsonMessageConverter-to-message]] +=== Converting to a `Message` + +As mentioned in the previous section, relying on Java serialization is generally not recommended. +One rather common alternative that is more flexible and portable across different languages and platforms is JSON +(JavaScript Object Notation). +The converter can be configured on any `RabbitTemplate` instance to override its usage of the `SimpleMessageConverter` +default. +The `Jackson2JsonMessageConverter` uses the `com.fasterxml.jackson` 2.x library. +The following example configures a `Jackson2JsonMessageConverter`: + +[source,xml] +---- + + + + + + + + + +---- + +As shown above, `Jackson2JsonMessageConverter` uses a `DefaultClassMapper` by default. +Type information is added to (and retrieved from) `MessageProperties`. +If an inbound message does not contain type information in `MessageProperties`, but you know the expected type, you +can configure a static type by using the `defaultType` property, as the following example shows: + +[source,xml] +---- + + + + + + + +---- + +In addition, you can provide custom mappings from the value in the `__TypeId__` header. +The following example shows how to do so: + +[source, java] +---- +@Bean +public Jackson2JsonMessageConverter jsonMessageConverter() { + Jackson2JsonMessageConverter jsonConverter = new Jackson2JsonMessageConverter(); + jsonConverter.setClassMapper(classMapper()); + return jsonConverter; +} + +@Bean +public DefaultClassMapper classMapper() { + DefaultClassMapper classMapper = new DefaultClassMapper(); + Map> idClassMapping = new HashMap<>(); + idClassMapping.put("thing1", Thing1.class); + idClassMapping.put("thing2", Thing2.class); + classMapper.setIdClassMapping(idClassMapping); + return classMapper; +} +---- + +Now, if the sending system sets the header to `thing1`, the converter creates a `Thing1` object, and so on. +See the xref:sample-apps.adoc#spring-rabbit-json[Receiving JSON from Non-Spring Applications] sample application for a complete discussion about converting messages from non-Spring applications. + +Starting with version 2.4.3, the converter will not add a `contentEncoding` message property if the `supportedMediaType` has a `charset` parameter; this is also used for the encoding. +A new method `setSupportedMediaType` has been added: + +[source, java] +---- +String utf16 = "application/json; charset=utf-16"; +converter.setSupportedContentType(MimeTypeUtils.parseMimeType(utf16)); +---- + +[[Jackson2JsonMessageConverter-from-message]] +=== Converting from a `Message` + +Inbound messages are converted to objects according to the type information added to headers by the sending system. + +Starting with version 2.4.3, if there is no `contentEncoding` message property, the converter will attempt to detect a `charset` parameter in the `contentType` message property and use that. +If neither exist, if the `supportedMediaType` has a `charset` parameter, it will be used for decoding, with a final fallback to the `defaultCharset` property. +A new method `setSupportedMediaType` has been added: + +[source, java] +---- +String utf16 = "application/json; charset=utf-16"; +converter.setSupportedContentType(MimeTypeUtils.parseMimeType(utf16)); +---- + +In versions prior to 1.6, if type information is not present, conversion would fail. +Starting with version 1.6, if type information is missing, the converter converts the JSON by using Jackson defaults (usually a map). + +Also, starting with version 1.6, when you use `@RabbitListener` annotations (on methods), the inferred type information is added to the `MessageProperties`. +This lets the converter convert to the argument type of the target method. +This only applies if there is one parameter with no annotations or a single parameter with the `@Payload` annotation. +Parameters of type `Message` are ignored during the analysis. + +IMPORTANT: By default, the inferred type information will override the inbound `__TypeId__` and related headers created +by the sending system. +This lets the receiving system automatically convert to a different domain object. +This applies only if the parameter type is concrete (not abstract or an interface) or it is from the `java.util` +package. +In all other cases, the `__TypeId__` and related headers is used. +There are cases where you might wish to override the default behavior and always use the `__TypeId__` information. +For example, suppose you have a `@RabbitListener` that takes a `Thing1` argument but the message contains a `Thing2` that +is a subclass of `Thing1` (which is concrete). +The inferred type would be incorrect. +To handle this situation, set the `TypePrecedence` property on the `Jackson2JsonMessageConverter` to `TYPE_ID` instead +of the default `INFERRED`. +(The property is actually on the converter's `DefaultJackson2JavaTypeMapper`, but a setter is provided on the converter +for convenience.) +If you inject a custom type mapper, you should set the property on the mapper instead. + +NOTE: When converting from the `Message`, an incoming `MessageProperties.getContentType()` must be JSON-compliant (`contentType.contains("json")` is used to check). +Starting with version 2.2, `application/json` is assumed if there is no `contentType` property, or it has the default value `application/octet-stream`. +To revert to the previous behavior (return an unconverted `byte[]`), set the converter's `assumeSupportedContentType` property to `false`. +If the content type is not supported, a `WARN` log message `Could not convert incoming message with content-type [...]`, is emitted and `message.getBody()` is returned as is -- as a `byte[]`. +So, to meet the `Jackson2JsonMessageConverter` requirements on the consumer side, the producer must add the `contentType` message property -- for example, as `application/json` or `text/x-json` or by using the `Jackson2JsonMessageConverter`, which sets the header automatically. +The following listing shows a number of converter calls: + +[source, java] +---- +@RabbitListener +public void thing1(Thing1 thing1) {...} + +@RabbitListener +public void thing1(@Payload Thing1 thing1, @Header("amqp_consumerQueue") String queue) {...} + +@RabbitListener +public void thing1(Thing1 thing1, o.s.amqp.core.Message message) {...} + +@RabbitListener +public void thing1(Thing1 thing1, o.s.messaging.Message message) {...} + +@RabbitListener +public void thing1(Thing1 thing1, String bar) {...} + +@RabbitListener +public void thing1(Thing1 thing1, o.s.messaging.Message message) {...} +---- + +In the first four cases in the preceding listing, the converter tries to convert to the `Thing1` type. +The fifth example is invalid because we cannot determine which argument should receive the message payload. +With the sixth example, the Jackson defaults apply due to the generic type being a `WildcardType`. + +You can, however, create a custom converter and use the `targetMethod` message property to decide which type to convert +the JSON to. + +NOTE: This type inference can only be achieved when the `@RabbitListener` annotation is declared at the method level. +With class-level `@RabbitListener`, the converted type is used to select which `@RabbitHandler` method to invoke. +For this reason, the infrastructure provides the `targetObject` message property, which you can use in a custom +converter to determine the type. + +IMPORTANT: Starting with version 1.6.11, `Jackson2JsonMessageConverter` and, therefore, `DefaultJackson2JavaTypeMapper` (`DefaultClassMapper`) provide the `trustedPackages` option to overcome https://pivotal.io/security/cve-2017-4995[Serialization Gadgets] vulnerability. +By default and for backward compatibility, the `Jackson2JsonMessageConverter` trusts all packages -- that is, it uses `*` for the option. + +Starting with version 2.4.7, the converter can be configured to return `Optional.empty()` if Jackson returns `null` after deserializing the message body. +This facilitates `@RabbitListener` s to receive null payloads, in two ways: + +[source, java] +---- +@RabbitListener(queues = "op.1") +void listen(@Payload(required = false) Thing payload) { + handleOptional(payload); // payload might be null +} + +@RabbitListener(queues = "op.2") +void listen(Optional optional) { + handleOptional(optional.orElse(this.emptyThing)); +} +---- + +To enable this feature, set `setNullAsOptionalEmpty` to `true`; when `false` (default), the converter falls back to the raw message body (`byte[]`). + +[source, java] +---- +@Bean +Jackson2JsonMessageConverter converter() { + Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter(); + converter.setNullAsOptionalEmpty(true); + return converter; +} +---- + +[[jackson-abstract]] +=== Deserializing Abstract Classes + +Prior to version 2.2.8, if the inferred type of a `@RabbitListener` was an abstract class (including interfaces), the converter would fall back to looking for type information in the headers and, if present, used that information; if that was not present, it would try to create the abstract class. +This caused a problem when a custom `ObjectMapper` that is configured with a custom deserializer to handle the abstract class is used, but the incoming message has invalid type headers. + +Starting with version 2.2.8, the previous behavior is retained by default. If you have such a custom `ObjectMapper` and you want to ignore type headers, and always use the inferred type for conversion, set the `alwaysConvertToInferredType` to `true`. +This is needed for backwards compatibility and to avoid the overhead of an attempted conversion when it would fail (with a standard `ObjectMapper`). + +[[data-projection]] +=== Using Spring Data Projection Interfaces + +Starting with version 2.2, you can convert JSON to a Spring Data Projection interface instead of a concrete type. +This allows very selective, and low-coupled bindings to data, including the lookup of values from multiple places inside the JSON document. +For example the following interface can be defined as message payload type: + +[source, java] +---- +interface SomeSample { + + @JsonPath({ "$.username", "$.user.name" }) + String getUsername(); + +} +---- + +[source, java] +---- +@RabbitListener(queues = "projection") +public void projection(SomeSample in) { + String username = in.getUsername(); + ... +} +---- + +Accessor methods will be used to lookup the property name as field in the received JSON document by default. +The `@JsonPath` expression allows customization of the value lookup, and even to define multiple JSON path expressions, to lookup values from multiple places until an expression returns an actual value. + +To enable this feature, set the `useProjectionForInterfaces` to `true` on the message converter. +You must also add `spring-data:spring-data-commons` and `com.jayway.jsonpath:json-path` to the class path. + +When used as the parameter to a `@RabbitListener` method, the interface type is automatically passed to the converter as normal. + +[[json-complex]] +=== Converting From a `Message` With `RabbitTemplate` + +As mentioned earlier, type information is conveyed in message headers to assist the converter when converting from a message. +This works fine in most cases. +However, when using generic types, it can only convert simple objects and known "`container`" objects (lists, arrays, and maps). +Starting with version 2.0, the `Jackson2JsonMessageConverter` implements `SmartMessageConverter`, which lets it be used with the new `RabbitTemplate` methods that take a `ParameterizedTypeReference` argument. +This allows conversion of complex generic types, as shown in the following example: + +[source, java] +---- +Thing1> thing1 = + rabbitTemplate.receiveAndConvert(new ParameterizedTypeReference>>() { }); +---- + +NOTE: Starting with version 2.1, the `AbstractJsonMessageConverter` class has been removed. +It is no longer the base class for `Jackson2JsonMessageConverter`. +It has been replaced by `AbstractJackson2MessageConverter`. + +[[marshallingmessageconverter]] +== `MarshallingMessageConverter` + +Yet another option is the `MarshallingMessageConverter`. +It delegates to the Spring OXM library's implementations of the `Marshaller` and `Unmarshaller` strategy interfaces. +You can read more about that library https://docs.spring.io/spring/docs/current/spring-framework-reference/html/oxm.html[here]. +In terms of configuration, it is most common to provide only the constructor argument, since most implementations of `Marshaller` also implement `Unmarshaller`. +The following example shows how to configure a `MarshallingMessageConverter`: + +[source,xml] +---- + + + + + + + + +---- + +[[jackson2xml]] +== `Jackson2XmlMessageConverter` + +This class was introduced in version 2.1 and can be used to convert messages from and to XML. + +Both `Jackson2XmlMessageConverter` and `Jackson2JsonMessageConverter` have the same base class: `AbstractJackson2MessageConverter`. + +NOTE: The `AbstractJackson2MessageConverter` class is introduced to replace a removed class: `AbstractJsonMessageConverter`. + +The `Jackson2XmlMessageConverter` uses the `com.fasterxml.jackson` 2.x library. + +You can use it the same way as `Jackson2JsonMessageConverter`, except it supports XML instead of JSON. +The following example configures a `Jackson2JsonMessageConverter`: + +[source,xml] +---- + + + + + + + +---- +See <> for more information. + +NOTE: Starting with version 2.2, `application/xml` is assumed if there is no `contentType` property, or it has the default value `application/octet-stream`. +To revert to the previous behavior (return an unconverted `byte[]`), set the converter's `assumeSupportedContentType` property to `false`. + +[[contenttypedelegatingmessageconverter]] +== `ContentTypeDelegatingMessageConverter` + +This class was introduced in version 1.4.2 and allows delegation to a specific `MessageConverter` based on the content type property in the `MessageProperties`. +By default, it delegates to a `SimpleMessageConverter` if there is no `contentType` property or there is a value that matches none of the configured converters. +The following example configures a `ContentTypeDelegatingMessageConverter`: + +[source,xml] +---- + + + + + + + + +---- + +[[java-deserialization]] +== Java Deserialization + +This section covers how to deserialize Java objects. + +[IMPORTANT] +==== +There is a possible vulnerability when deserializing java objects from untrusted sources. + +If you accept messages from untrusted sources with a `content-type` of `application/x-java-serialized-object`, you should +consider configuring which packages and classes are allowed to be deserialized. +This applies to both the `SimpleMessageConverter` and `SerializerMessageConverter` when it is configured to use a +`DefaultDeserializer` either implicitly or via configuration. + +By default, the allowed list is empty, meaning no classes will be deserialized. + +You can set a list of patterns, such as `thing1.*`, `thing1.thing2.Cat` or `*.MySafeClass`. + +The patterns are checked in order until a match is found. +If there is no match, a `SecurityException` is thrown. + +You can set the patterns using the `allowedListPatterns` property on these converters. +Alternatively, if you trust all message originators, you can set the environment variable `SPRING_AMQP_DESERIALIZATION_TRUST_ALL` or system property `spring.amqp.deserialization.trust.all` to `true`. +==== + +[[message-properties-converters]] +== Message Properties Converters + +The `MessagePropertiesConverter` strategy interface is used to convert between the Rabbit Client `BasicProperties` and Spring AMQP `MessageProperties`. +The default implementation (`DefaultMessagePropertiesConverter`) is usually sufficient for most purposes, but you can implement your own if needed. +The default properties converter converts `BasicProperties` elements of type `LongString` to `String` instances when the size is not greater than `1024` bytes. +Larger `LongString` instances are not converted (see the next paragraph). +This limit can be overridden with a constructor argument. + +Starting with version 1.6, headers longer than the long string limit (default: 1024) are now left as +`LongString` instances by default by the `DefaultMessagePropertiesConverter`. +You can access the contents through the `getBytes[]`, `toString()`, or `getStream()` methods. + +Previously, the `DefaultMessagePropertiesConverter` "`converted`" such headers to a `DataInputStream` (actually it just referenced the `LongString` instance's `DataInputStream`). +On output, this header was not converted (except to a String -- for example, `java.io.DataInputStream@1d057a39` by calling `toString()` on the stream). + +Large incoming `LongString` headers are now correctly "`converted`" on output, too (by default). + +A new constructor is provided to let you configure the converter to work as before. +The following listing shows the Javadoc comment and declaration of the method: + +[source, java] +---- +/** + * 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) { ... } +---- + +Also starting with version 1.6, a new property called `correlationIdString` has been added to `MessageProperties`. +Previously, when converting to and from `BasicProperties` used by the RabbitMQ client, an unnecessary `byte[] <-> String` conversion was performed because `MessageProperties.correlationId` is a `byte[]`, but `BasicProperties` uses a `String`. +(Ultimately, the RabbitMQ client uses UTF-8 to convert the `String` to bytes to put in the protocol message). + +To provide maximum backwards compatibility, a new property called `correlationIdPolicy` has been added to the +`DefaultMessagePropertiesConverter`. +This takes a `DefaultMessagePropertiesConverter.CorrelationIdPolicy` enum argument. +By default it is set to `BYTES`, which replicates the previous behavior. + +For inbound messages: + +* `STRING`: Only the `correlationIdString` property is mapped +* `BYTES`: Only the `correlationId` property is mapped +* `BOTH`: Both properties are mapped + +For outbound messages: + +* `STRING`: Only the `correlationIdString` property is mapped +* `BYTES`: Only the `correlationId` property is mapped +* `BOTH`: Both properties are considered, with the `String` property taking precedence + +Also starting with version 1.6, the inbound `deliveryMode` property is no longer mapped to `MessageProperties.deliveryMode`. +It is mapped to `MessageProperties.receivedDeliveryMode` instead. +Also, the inbound `userId` property is no longer mapped to `MessageProperties.userId`. +It is mapped to `MessageProperties.receivedUserId` instead. +These changes are to avoid unexpected propagation of these properties if the same `MessageProperties` object is used for an outbound message. + +Starting with version 2.2, the `DefaultMessagePropertiesConverter` converts any custom headers with values of type `Class` using `getName()` instead of `toString()`; this avoids consuming application having to parse the class name out of the `toString()` representation. +For rolling upgrades, you may need to change your consumers to understand both formats until all producers are upgraded. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/multi-rabbit.adoc b/src/reference/antora/modules/ROOT/pages/amqp/multi-rabbit.adoc new file mode 100644 index 0000000000..9d1a97d940 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/multi-rabbit.adoc @@ -0,0 +1,149 @@ +[[multi-rabbit]] += Multiple Broker (or Cluster) Support + +Version 2.3 added more convenience when communicating between a single application and multiple brokers or broker clusters. +The main benefit, on the consumer side, is that the infrastructure can automatically associate auto-declared queues with the appropriate broker. + +This is best illustrated with an example: + +[source, java] +---- +@SpringBootApplication(exclude = RabbitAutoConfiguration.class) +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + CachingConnectionFactory cf1() { + return new CachingConnectionFactory("localhost"); + } + + @Bean + CachingConnectionFactory cf2() { + return new CachingConnectionFactory("otherHost"); + } + + @Bean + CachingConnectionFactory cf3() { + return new CachingConnectionFactory("thirdHost"); + } + + @Bean + SimpleRoutingConnectionFactory rcf(CachingConnectionFactory cf1, + CachingConnectionFactory cf2, CachingConnectionFactory cf3) { + + SimpleRoutingConnectionFactory rcf = new SimpleRoutingConnectionFactory(); + rcf.setDefaultTargetConnectionFactory(cf1); + rcf.setTargetConnectionFactories(Map.of("one", cf1, "two", cf2, "three", cf3)); + return rcf; + } + + @Bean("factory1-admin") + RabbitAdmin admin1(CachingConnectionFactory cf1) { + return new RabbitAdmin(cf1); + } + + @Bean("factory2-admin") + RabbitAdmin admin2(CachingConnectionFactory cf2) { + return new RabbitAdmin(cf2); + } + + @Bean("factory3-admin") + RabbitAdmin admin3(CachingConnectionFactory cf3) { + return new RabbitAdmin(cf3); + } + + @Bean + public RabbitListenerEndpointRegistry rabbitListenerEndpointRegistry() { + return new RabbitListenerEndpointRegistry(); + } + + @Bean + public RabbitListenerAnnotationBeanPostProcessor postProcessor(RabbitListenerEndpointRegistry registry) { + MultiRabbitListenerAnnotationBeanPostProcessor postProcessor + = new MultiRabbitListenerAnnotationBeanPostProcessor(); + postProcessor.setEndpointRegistry(registry); + postProcessor.setContainerFactoryBeanName("defaultContainerFactory"); + return postProcessor; + } + + @Bean + public SimpleRabbitListenerContainerFactory factory1(CachingConnectionFactory cf1) { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(cf1); + return factory; + } + + @Bean + public SimpleRabbitListenerContainerFactory factory2(CachingConnectionFactory cf2) { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(cf2); + return factory; + } + + @Bean + public SimpleRabbitListenerContainerFactory factory3(CachingConnectionFactory cf3) { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(cf3); + return factory; + } + + @Bean + RabbitTemplate template(SimpleRoutingConnectionFactory rcf) { + return new RabbitTemplate(rcf); + } + + @Bean + ConnectionFactoryContextWrapper wrapper(SimpleRoutingConnectionFactory rcf) { + return new ConnectionFactoryContextWrapper(rcf); + } + +} + +@Component +class Listeners { + + @RabbitListener(queuesToDeclare = @Queue("q1"), containerFactory = "factory1") + public void listen1(String in) { + + } + + @RabbitListener(queuesToDeclare = @Queue("q2"), containerFactory = "factory2") + public void listen2(String in) { + + } + + @RabbitListener(queuesToDeclare = @Queue("q3"), containerFactory = "factory3") + public void listen3(String in) { + + } + +} +---- + +As you can see, we have declared 3 sets of infrastructure (connection factories, admins, container factories). +As discussed earlier, `@RabbitListener` can define which container factory to use; in this case, they also use `queuesToDeclare` which causes the queue(s) to be declared on the broker, if it doesn't exist. +By naming the `RabbitAdmin` beans with the convention `-admin`, the infrastructure is able to determine which admin should declare the queue. +This will also work with `bindings = @QueueBinding(...)` whereby the exchange and binding will also be declared. +It will NOT work with `queues`, since that expects the queue(s) to already exist. + +On the producer side, a convenient `ConnectionFactoryContextWrapper` class is provided, to make using the `RoutingConnectionFactory` (see <>) simpler. + +As you can see above, a `SimpleRoutingConnectionFactory` bean has been added with routing keys `one`, `two` and `three`. +There is also a `RabbitTemplate` that uses that factory. +Here is an example of using that template with the wrapper to route to one of the broker clusters. + +[source, java] +---- +@Bean +public ApplicationRunner runner(RabbitTemplate template, ConnectionFactoryContextWrapper wrapper) { + return args -> { + wrapper.run("one", () -> template.convertAndSend("q1", "toCluster1")); + wrapper.run("two", () -> template.convertAndSend("q2", "toCluster2")); + wrapper.run("three", () -> template.convertAndSend("q3", "toCluster3")); + }; +} +---- + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/post-processing.adoc b/src/reference/antora/modules/ROOT/pages/amqp/post-processing.adoc new file mode 100644 index 0000000000..7f01e783cb --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/post-processing.adoc @@ -0,0 +1,35 @@ +[[post-processing]] += Modifying Messages - Compression and More + +A number of extension points exist. +They let you perform some processing on a message, either before it is sent to RabbitMQ or immediately after it is received. + +As can be seen in xref:amqp/message-converters.adoc[Message Converters], one such extension point is in the `AmqpTemplate` `convertAndReceive` operations, where you can provide a `MessagePostProcessor`. +For example, after your POJO has been converted, the `MessagePostProcessor` lets you set custom headers or properties on the `Message`. + +Starting with version 1.4.2, additional extension points have been added to the `RabbitTemplate` - `setBeforePublishPostProcessors()` and `setAfterReceivePostProcessors()`. +The first enables a post processor to run immediately before sending to RabbitMQ. +When using batching (see xref:amqp/sending-messages.adoc#template-batching[Batching]), this is invoked after the batch is assembled and before the batch is sent. +The second is invoked immediately after a message is received. + +These extension points are used for such features as compression and, for this purpose, several `MessagePostProcessor` implementations are provided. +`GZipPostProcessor`, `ZipPostProcessor` and `DeflaterPostProcessor` compress messages before sending, and `GUnzipPostProcessor`, `UnzipPostProcessor` and `InflaterPostProcessor` decompress received messages. + +NOTE: Starting with version 2.1.5, the `GZipPostProcessor` can be configured with the `copyProperties = true` option to make a copy of the original message properties. +By default, these properties are reused for performance reasons, and modified with compression content encoding and the optional `MessageProperties.SPRING_AUTO_DECOMPRESS` header. +If you retain a reference to the original outbound message, its properties will change as well. +So, if your application retains a copy of an outbound message with these message post processors, consider turning the `copyProperties` option on. + +IMPORTANT: Starting with version 2.2.12, you can configure the delimiter that the compressing post processors use between content encoding elements. +With versions 2.2.11 and before, this was hard-coded as `:`, it is now set to `, ` by default. +The decompressors will work with both delimiters. +However, if you publish messages with 2.3 or later and consume with 2.2.11 or earlier, you MUST set the `encodingDelimiter` property on the compressor(s) to `:`. +When your consumers are upgraded to 2.2.11 or later, you can revert to the default of `, `. + +Similarly, the `SimpleMessageListenerContainer` also has a `setAfterReceivePostProcessors()` method, letting the decompression be performed after messages are received by the container. + +Starting with version 2.1.4, `addBeforePublishPostProcessors()` and `addAfterReceivePostProcessors()` have been added to the `RabbitTemplate` to allow appending new post processors to the list of before publish and after receive post processors respectively. +Also there are methods provided to remove the post processors. +Similarly, `AbstractMessageListenerContainer` also has `addAfterReceivePostProcessors()` and `removeAfterReceivePostProcessor()` methods added. +See the Javadoc of `RabbitTemplate` and `AbstractMessageListenerContainer` for more detail. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages.adoc new file mode 100644 index 0000000000..271e10a67f --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages.adoc @@ -0,0 +1,10 @@ +[[receiving-messages]] += Receiving Messages +:page-section-summary-toc: 1 + +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. +We consider an example of each approach in the next two sub-sections. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven.adoc new file mode 100644 index 0000000000..35c6691d2a --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven.adoc @@ -0,0 +1,124 @@ +[[async-annotation-driven]] += Annotation-driven Listener Endpoints + +The easiest way to receive a message asynchronously is to use the annotated listener endpoint infrastructure. +In a nutshell, it lets you expose a method of a managed bean as a Rabbit listener endpoint. +The following example shows how to use the `@RabbitListener` annotation: + +[source,java] +---- + +@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. + +NOTE: Property placeholders (`${some.property}`) or SpEL expressions (`+#{someExpression}+`) can be specified for the annotation properties (`queues` etc). +See xref:amqp/receiving-messages/async-annotation-driven/multiple-queues.adoc[Listening to Multiple Queues] for an example of why you might use SpEL instead of a property placeholder. +The following listing shows three examples of how to declare a Rabbit listener: + +[source,java] +---- + +@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(Order order) { + ... + } + + @RabbitListener(bindings = @QueueBinding( + value = @Queue, + exchange = @Exchange(value = "auto.exch"), + key = "invoiceRoutingKey") + ) + public void processInvoice(Invoice invoice) { + ... + } + + @RabbitListener(queuesToDeclare = @Queue(name = "${my.queue}", durable = "true")) + public String handleWithSimpleDeclare(String data) { + ... + } + +} +---- + +In the first example, a queue `myQueue` is declared automatically (durable) together with the exchange, if needed, +and bound to the exchange with the routing key. +In the second example, 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; see xref:amqp/containers-and-broker-named-queues.adoc[Containers and Broker-Named queues]. +Multiple `QueueBinding` entries can be provided, letting the listener listen to multiple queues. +In the third example, 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. + +Since version 2.0, the `@Exchange` annotation supports any exchange types, including custom. +For more information, see https://www.rabbitmq.com/tutorials/amqp-concepts.html[AMQP Concepts]. + +You can use normal `@Bean` definitions when you need more advanced configuration. + +Notice `ignoreDeclarationExceptions` on the exchange in the first example. +This allows, for example, binding to an existing exchange that might have different settings (such as `internal`). +By default, the properties of an existing exchange must match. + +Starting with version 2.0, you can now bind a queue to an exchange with multiple routing keys, as the following example shows: + +[source, java] +---- +... + key = { "red", "yellow" } +... +---- + +You can also specify arguments within `@QueueBinding` annotations for queues, exchanges, +and bindings, as the following example shows: + +[source, java] +---- +@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 = "thing1", value = "somevalue"), + @Argument(name = "thing2") + }) +) +public String handleWithHeadersExchange(String foo) { + ... +} +---- + +Notice that the `x-message-ttl` argument is set to 10 seconds for the queue. +Since the argument type is not `String`, we have to specify its type -- in this case, `Integer`. +As with all such declarations, if the queue already exists, the arguments must match those on the queue. +For the header exchange, we set the binding arguments to match messages that have the `thing1` header set to `somevalue`, and +the `thing2` header must be present with any value. +The `x-match` argument means both conditions must be satisfied. + +The argument name, value, and type can be property placeholders (`${...}`) or SpEL expressions (`#{...}`). +The `name` must resolve to a `String`. +The expression for `type` must resolve to a `Class` or the fully-qualified name of a class. +The `value` must resolve to something that can be converted by the `DefaultConversionService` to the type (such as the `x-message-ttl` in the preceding example). + +If a name resolves to `null` or an empty `String`, that `@Argument` is ignored. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/container-management.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/container-management.adoc new file mode 100644 index 0000000000..2b4a69c857 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/container-management.adoc @@ -0,0 +1,23 @@ +[[container-management]] += Container Management +:page-section-summary-toc: 1 + +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. +You can then iterate over this collection, for example, to stop or start all containers or invoke the `Lifecycle` methods +on the registry itself, which will invoke the operations on each container. + +You can also get a reference to an individual container by using its `id`, using `getListenerContainer(String id)` -- for +example, `registry.getListenerContainer("multi")` for the container created by the snippet above. + +Starting with version 1.5.2, you can obtain the `id` values of the registered containers with `getListenerContainerIds()`. + +Starting with version 1.5, you can now assign a `group` to the container on the `RabbitListener` endpoint. +This provides a mechanism to get a reference to a subset of containers. +Adding a `group` attribute causes a bean of type `Collection` to be registered with the context with the group name. + +By default, stopping a container will cancel the consumer and process all prefetched messages before stopping. +Starting with versions 2.4.14, 3.0.6, you can set the <> 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. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/conversion.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/conversion.adoc new file mode 100644 index 0000000000..8f2adbe6de --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/conversion.adoc @@ -0,0 +1,101 @@ +[[async-annotation-conversion]] += Message Conversion for Annotated Methods + +There are two conversion steps in the pipeline before invoking the listener. +The first step uses a `MessageConverter` to convert the incoming Spring AMQP `Message` to a Spring-messaging `Message`. +When the target method is invoked, the message payload is converted, if necessary, to the method parameter type. + +The default `MessageConverter` for the first step is a Spring AMQP `SimpleMessageConverter` that handles conversion to +`String` and `java.io.Serializable` objects. +All others remain as a `byte[]`. +In the following discussion, we call this the "`message converter`". + +The default converter for the second step is a `GenericMessageConverter`, which delegates to a conversion service +(an instance of `DefaultFormattingConversionService`). +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. +The following example shows how to do so: + +[source, java] +---- +@Bean +public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + ... + factory.setMessageConverter(new Jackson2JsonMessageConverter()); + ... + return factory; +} +---- + +This configures a Jackson2 converter that expects header information to be present to guide the conversion. + +You can also use a `ContentTypeDelegatingMessageConverter`, which can handle conversion of different content types. + +Starting with version 2.3, you can override the factory converter by specifying a bean name in the `messageConverter` property. + +[source, java] +---- +@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. + +NOTE: This type inference works only for `@RabbitListener` at the method level. + +See <> for more information. + +If you wish to customize the method argument converter, you can do so as follows: + +[source, java] +---- +@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()); + } + + ... + +} +---- + +IMPORTANT: For multi-method listeners (see xref:amqp/receiving-messages/async-annotation-driven/method-selection.adoc[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. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/custom-argument-resolver.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/custom-argument-resolver.adoc new file mode 100644 index 0000000000..8ee48932f3 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/custom-argument-resolver.adoc @@ -0,0 +1,36 @@ +[[custom-argument-resolver]] += Adding a Custom `HandlerMethodArgumentResolver` to @RabbitListener + +Starting with version 2.3.7 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`. + +[source, java] +---- +@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) + ); + } + + } + ); + } + +} +---- + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/enable-signature.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/enable-signature.adoc new file mode 100644 index 0000000000..ed5464b829 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/enable-signature.adoc @@ -0,0 +1,71 @@ +[[async-annotation-driven-enable-signature]] += 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. +The following example rewrites it to inject the `Order` with a custom header: + +[source,java] +---- +@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: + +* The raw `org.springframework.amqp.core.Message`. +* The `MessageProperties` from the raw `Message`. +* The `com.rabbitmq.client.Channel` on which the message was received. +* The `org.springframework.messaging.Message` converted from the incoming AMQP message. +* `@Header`-annotated method arguments to extract a specific header value, including standard AMQP headers. +* `@Headers`-annotated argument that must also 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. +The following example shows how to do so: + +[source,java] +---- + +@RabbitListener(queues = "myQueue") +public void processOrder(Message 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, as follows: + +[source,java] +---- + +@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; + } +} +---- + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/enable.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/enable.adoc new file mode 100644 index 0000000000..98890bf869 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/enable.adoc @@ -0,0 +1,121 @@ +[[async-annotation-driven-enable]] += Enable Listener Endpoint Annotations + +To enable support for `@RabbitListener` annotations, you can add `@EnableRabbit` to one of your `@Configuration` classes. +The following example shows how to do so: + +[source,java] +---- +@Configuration +@EnableRabbit +public class AppConfig { + + @Bean + public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(connectionFactory()); + factory.setConcurrentConsumers(3); + factory.setMaxConcurrentConsumers(10); + factory.setContainerCustomizer(container -> /* customize the container */); + return factory; + } +} +---- + +Since version 2.0, a `DirectMessageListenerContainerFactory` is also available. +It creates `DirectMessageListenerContainer` instances. + +NOTE: For information to help you choose between `SimpleRabbitListenerContainerFactory` and `DirectRabbitListenerContainerFactory`, see xref:amqp/receiving-messages/choose-container.adoc[Choosing a Container]. + +Starting with version 2.2.2, you can provide a `ContainerCustomizer` implementation (as shown above). +This can be used to further configure the container after it has been created and configured; you can use this, for example, to set properties that are not exposed by the container factory. + +Version 2.4.8 provides the `CompositeContainerCustomizer` for situations where you wish to apply multiple customizers. + +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. + +You can customize the listener container factory to use for each annotation, or you can configure an explicit default by implementing the `RabbitListenerConfigurer` interface. +The default is required only if at least one endpoint is registered without a specific container factory. +See the https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/rabbit/annotation/RabbitListenerConfigurer.html[Javadoc] for full details and examples. + +The container factories provide methods for adding `MessagePostProcessor` instances that are applied after receiving messages (before invoking the listener) and before sending replies. + +See xref:amqp/receiving-messages/async-annotation-driven/reply.adoc[Reply Management] for information about replies. + +Starting with version 2.0.6, 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. +You can use a `SendRetryContextAccessor` to get information from the context. +The following example shows how to do so: + +[source, java] +---- +factory.setRetryTemplate(retryTemplate); +factory.setReplyRecoveryCallback(ctx -> { + Message failed = SendRetryContextAccessor.getMessage(ctx); + Address replyTo = SendRetryContextAccessor.getAddress(ctx); + Throwable t = ctx.getLastThrowable(); + ... + return null; +}); +---- + +If you prefer XML configuration, you can use the `` element. +Any beans annotated with `@RabbitListener` are detected. + +For `SimpleRabbitListenerContainer` instances, you can use XML similar to the following: + +[source,xml] +---- + + + + + + + +---- + +For `DirectMessageListenerContainer` instances, you can use XML similar to the following: + +[source,xml] +---- + + + + + + +---- + + +[[listener-property-overrides]] +Starting with version 2.0, the `@RabbitListener` annotation has a `concurrency` property. +It supports SpEL expressions (`#{...}`) and property placeholders (`${...}`). +Its meaning and allowed values depend on the container type, as follows: + +* For the `DirectMessageListenerContainer`, the value must be a single integer value, which sets the `consumersPerQueue` property on the container. +* For the `SimpleRabbitListenerContainer`, the value can be a single integer value, which sets the `concurrentConsumers` property on the container, or it can have the form, `m-n`, where `m` is the `concurrentConsumers` property and `n` is the `maxConcurrentConsumers` property. + +In either case, this setting overrides the settings on the factory. +Previously you had to define different container factories if you had listeners that required different concurrency. + +The annotation also allows overriding the factory `autoStartup` and `taskExecutor` properties via the `autoStartup` and `executor` (since 2.2) annotation properties. +Using a different executor for each might help with identifying threads associated with each listener in logs and thread dumps. + +Version 2.2 also added the `ackMode` property, which allows you to override the container factory's `acknowledgeMode` property. + +[source, java] +---- +@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); +} +---- + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/error-handling.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/error-handling.adoc new file mode 100644 index 0000000000..9c26c69de0 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/error-handling.adoc @@ -0,0 +1,55 @@ +[[annotation-error-handling]] += 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. + +Starting with version 2.0, the `@RabbitListener` annotation has two new attributes: `errorHandler` and `returnExceptions`. + +These are not configured by default. + +You can use the `errorHandler` to provide the bean name of a `RabbitListenerErrorHandler` implementation. +This functional interface has one method, as follows: + +[source, java] +---- +@FunctionalInterface +public interface RabbitListenerErrorHandler { + + Object handleError(Message amqpMessage, org.springframework.messaging.Message message, + ListenerExecutionFailedException exception) throws Exception; + +} +---- + +As you can see, you have access to the raw message received from the container, the spring-messaging `Message` object produced by the message converter, and 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. + +IMPORTANT: 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. + +IMPORTANT: In version 2.1, this interface moved from package `o.s.amqp.rabbit.listener` to `o.s.amqp.rabbit.listener.api`. + +Starting with version 2.1.7, the `Channel` is available in a messaging message header; this allows you to ack or nack the failed messasge when using `AcknowledgeMode.MANUAL`: + +[source, java] +---- +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); + } +---- + +Starting with version 2.2.18, 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. +Previously, such errors were thrown and handled by the container. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/meta.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/meta.adoc new file mode 100644 index 0000000000..9ef2318ba3 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/meta.adoc @@ -0,0 +1,71 @@ +[[meta-annotation-driven]] += 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. +The following example shows how to do so: + +[source, java] +---- +@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) { + ... + } + +} +---- + +In the preceding example, each listener created by the `@MyAnonFanoutListener` annotation binds an anonymous, auto-delete +queue to the fanout exchange, `metaFanout`. +Starting with version 2.2.3, `@AliasFor` is supported to allow overriding properties on the meta-annotated annotation. +Also, user annotations can now be `@Repeatable`, allowing multiple containers to be created for a method. + +[source, java] +---- +@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(); + +} +---- + + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/method-selection.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/method-selection.adoc new file mode 100644 index 0000000000..0fba2986f2 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/method-selection.adoc @@ -0,0 +1,47 @@ +[[annotation-method-selection]] += Multi-method Listeners + +Starting with version 1.5.0, 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. +This is best described using an example: + +[source, java] +---- +@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` (xref:amqp/receiving-messages/async-consumer.adoc#message-listener-adapter[described earlier]). + +Starting with version 2.0.3, 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. + +IMPORTANT: `@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. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/multiple-queues.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/multiple-queues.adoc new file mode 100644 index 0000000000..8ac6879654 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/multiple-queues.adoc @@ -0,0 +1,40 @@ +[[annotation-multiple-queues]] += 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. +The following example shows how to do so: + +[source, java] +---- +@Component +public class MyService { + + @RabbitListener(queues = { "queue1", "queue2" } ) + public void processOrder(String data, @Header(AmqpHeaders.CONSUMER_QUEUE) String queue) { + ... + } + +} +---- + +Starting with version 1.5, you can externalize the queue names by using property placeholders and SpEL. +The following example shows how to do so: + +[source, java] +---- +@Component +public class MyService { + + @RabbitListener(queues = "#{'${property.with.comma.delimited.queue.names}'.split(',')}" ) + public void processOrder(String data, @Header(AmqpHeaders.CONSUMER_QUEUE) String queue) { + ... + } + +} +---- + +Prior to version 1.5, only a single queue could be specified this way. +Each queue needed a separate property. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/proxy-rabbitlistener-and-generics.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/proxy-rabbitlistener-and-generics.adoc new file mode 100644 index 0000000000..0f625de9a8 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/proxy-rabbitlistener-and-generics.adoc @@ -0,0 +1,46 @@ +[[proxy-rabbitlistener-and-generics]] += 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. +Consider the following example: + +[source, java] +---- +interface TxService

{ + + String handle(P payload, String header); + +} + +static class TxServiceImpl implements TxService { + + @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, as the following example shows: + +[source, java] +---- +static class TxServiceImpl implements TxService { + + @Override + @Transactional + @RabbitListener(...) + public String handle(@Payload Foo foo, @Header("amqp_receivedRoutingKey") String rk) { + ... + } + +} +---- + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/rabbit-validation.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/rabbit-validation.adoc new file mode 100644 index 0000000000..784c8c8305 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/rabbit-validation.adoc @@ -0,0 +1,69 @@ +[[rabbit-validation]] += @RabbitListener @Payload Validation + +Starting with version 2.3.7, 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. + +[source, java] +---- +@Configuration +@EnableRabbit +public class Config implements RabbitListenerConfigurer { + ... + @Override + public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) { + registrar.setValidator(new MyValidator()); + } +} +---- + +NOTE: When using Spring Boot with the validation starter, a `LocalValidatorFactoryBean` is auto-configured: + +[source, java] +---- +@Configuration +@EnableRabbit +public class Config implements RabbitListenerConfigurer { + @Autowired + private LocalValidatorFactoryBean validator; + ... + @Override + public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) { + registrar.setValidator(this.validator); + } +} +---- + +To validate: + +[source, java] +---- +public static class ValidatedClass { + @Max(10) + private int bar; + public int getBar() { + return this.bar; + } + public void setBar(int bar) { + this.bar = bar; + } +} +---- + +and + +[source, java] +---- +@RabbitListener(id="validated", queues = "queue1", errorHandler = "validationErrorHandler", + containerFactory = "jsonListenerContainerFactory") +public void validatedListener(@Payload @Valid ValidatedClass val) { + ... +} +@Bean +public RabbitListenerErrorHandler validationErrorHandler() { + return (m, e) -> { + ... + }; +} +---- + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/registration.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/registration.adoc new file mode 100644 index 0000000000..d1458af838 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/registration.adoc @@ -0,0 +1,29 @@ +[[async-annotation-driven-registration]] += 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. +The following example shows how to do so: + +[source,java] +---- +@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); + } +} +---- + +In the preceding example, we used `SimpleRabbitListenerEndpoint`, which provides the actual `MessageListener` to invoke, but you could just as well build your own endpoint variant to describe a custom invocation mechanism. + +It should be noted that you could just as well skip the use of `@RabbitListener` altogether and register your endpoints programmatically through `RabbitListenerConfigurer`. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/repeatable-rabbit-listener.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/repeatable-rabbit-listener.adoc new file mode 100644 index 0000000000..cac86f7174 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/repeatable-rabbit-listener.adoc @@ -0,0 +1,10 @@ +[[repeatable-rabbit-listener]] += `@Repeatable` `@RabbitListener` +:page-section-summary-toc: 1 + +Starting with version 1.6, 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, each of which invokes the same listener +`@Bean`. +Repeatable annotations can be used with Java 8 or above. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/reply-content-type.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/reply-content-type.adoc new file mode 100644 index 0000000000..b8fc03bd19 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/reply-content-type.adoc @@ -0,0 +1,51 @@ +[[reply-content-type]] += 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. +This allows the converter to select the appropriate delegate converter for the reply. + +[source, java] +---- +@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. + +[source, java] +---- +@RabbitListener(queues = "q1", replyContentType = "application/json", + converterWinsContentType = "false") +public String listen(Thing in) { + ... + return someJsonString; +} +---- + +These properties (`replyContentType` and `converterWinsContentType`) do not apply when the return type is a Spring AMQP `Message` or a Spring Messaging `Message`. +In the first case, there is no conversion involved; simply set the `contentType` message property. +In the second case, the behavior is controlled using message headers: + +[source, java] +---- +@RabbitListener(queues = "q1", messageConverter = "delegating") +@SendTo("q2") +public Message listen(String in) { + ... + return MessageBuilder.withPayload(in.toUpperCase()) + .setHeader(MessageHeaders.CONTENT_TYPE, "application/xml") + .build(); +} +---- + +This content type will be passed in the `MessageProperties` to the converter. +By default, for backwards compatibility, any content type property set by the converter will be overwritten by this value after conversion. +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. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/reply.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/reply.adoc new file mode 100644 index 0000000000..6c9c584614 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/reply.adoc @@ -0,0 +1,158 @@ +[[async-annotation-driven-reply]] += 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. + +Assuming our `processOrder` method should now return an `OrderStatus`, we can write it as follows to automatically send a reply: + +[source,java] +---- +@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, something like the following: + +[source,java] +---- + +@RabbitListener(destination = "myQueue") +@SendTo("status") +public Message 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. +Starting with version 2.2.3, 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: + +[source, java] +---- +factory.setBeforeSendReplyPostProcessors(msg -> { + msg.getMessageProperties().setHeader("calledBean", + msg.getMessageProperties().getTargetBean().getClass().getSimpleName()); + msg.getMessageProperties().setHeader("calledMethod", + msg.getMessageProperties().getTargetMethod().getName()); + return m; +}); +---- + +Starting with version 2.2.5, 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. + +[source, java] +---- +@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; + }; +} +---- + +Starting with version 3.0, you can configure the post processor on the container factory instead of on the annotation. + +[source, java] +---- +factory.setReplyPostProcessorProvider(id -> (req, resp) -> { + resp.getMessageProperties().setHeader("myHeader", req.getMessageProperties().getHeader("myHeader")); + return resp; +}); +---- + +The `id` parameter is the listener id. + +A setting on the annotation will supersede the factory setting. + +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. +The valid values are as follows: + +* `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, as shown in the following example: + +[source, java] +---- +@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 must evaluate to a `String`, which can be a simple queue name (sent to the default exchange) or with +the form `exchange/routingKey` as discussed prior to the preceding example. + +NOTE: 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 (described after the next example). + +Starting with version 1.6, the `@SendTo` can be a SpEL expression that is evaluated at runtime against the request +and reply, as the following example shows: + +[source, java] +---- +@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. + +The context has a map property accessor, a standard type converter, and a bean resolver, which lets other beans in the +context be referenced (for example, `@someBeanName.determineReplyQ(request, 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 `@`. + +Starting with version 2.1, simple property placeholders are also supported (for example, `${some.reply.to}`). +With earlier versions, the following can be used as a work around, as the following example shows: + +[source, java] +---- +@RabbitListener(queues = "foo") +@SendTo("#{environment['my.send.to']}") +public String listen(Message in) { + ... + return ... +} +---- + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-consumer.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-consumer.adoc new file mode 100644 index 0000000000..5c11273f91 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-consumer.adoc @@ -0,0 +1,275 @@ +[[async-consumer]] += Asynchronous Consumer + +IMPORTANT: 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. +See xref:amqp/receiving-messages/async-annotation-driven.adoc[Annotation-driven Listener Endpoints] for more details. + +[IMPORTANT] +==== +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. + +See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration]. + +For more background about prefetch, see this post about https://www.rabbitmq.com/blog/2014/04/14/finding-bottlenecks-with-rabbitmq-3-3/[consumer utilization in RabbitMQ] +and this post about https://www.rabbitmq.com/blog/2012/05/11/some-queuing-theory-throughput-latency-and-bandwidth/[queuing theory]. +==== + +[[message-listener]] +== Message Listener + +For asynchronous `Message` reception, a dedicated component (not the `AmqpTemplate`) is involved. +That component is a container for a `Message`-consuming callback. +We consider the container and its properties later in this section. +First, though, we should look at the callback, since that is where your application code is integrated with the messaging system. +There are a few options for the callback, starting with an implementation of the `MessageListener` interface, which the following listing shows: + +[source,java] +---- +public interface MessageListener { + void onMessage(Message message); +} +---- + +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. +The following listing shows the `ChannelAwareMessageListener` interface definition: + +[source,java] +---- +public interface ChannelAwareMessageListener { + void onMessage(Message message, Channel channel) throws Exception; +} +---- + +IMPORTANT: In version 2.1, this interface moved from package `o.s.amqp.rabbit.core` to `o.s.amqp.rabbit.listener.api`. + +[[message-listener-adapter]] +== `MessageListenerAdapter` + +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. + +NOTE: Version 1.5 introduced a more flexible mechanism for POJO messaging, the `@RabbitListener` annotation. +See xref:amqp/receiving-messages/async-annotation-driven.adoc[Annotation-driven Listener Endpoints] for more information. + +When using the adapter, you need to provide only a reference to the instance that the adapter itself should invoke. +The following example shows how to do so: + +[source,java] +---- +MessageListenerAdapter listener = new MessageListenerAdapter(somePojo); +listener.setDefaultListenerMethod("myMethod"); +---- + +You can subclass the adapter and provide an implementation of `getListenerMethodName()` to dynamically select different methods based on the message. +This method has two parameters, `originalMessage` and `extractedMessage`, the latter being the result of any conversion. +By default, a `SimpleMessageConverter` is configured. +See xref:amqp/message-converters.adoc#simple-message-converter[`SimpleMessageConverter`] for more information and information about other converters available. + +Starting with version 1.4.2, the original message has `consumerQueue` and `consumerTag` properties, which can be used to determine the queue from which a message was received. + +Starting with version 1.5, you can configure a map of consumer queue or tag to method name, to dynamically select the method to call. +If no entry is in the map, we fall back to the default listener method. +The default listener method (if not set) is `handleMessage`. + +Starting with version 2.0, a convenient `FunctionalInterface` has been provided. +The following listing shows the definition of `FunctionalInterface`: + +[source, java] +---- +@FunctionalInterface +public interface ReplyingMessageListener { + + R handleMessage(T t); + +} +---- + +This interface facilitates convenient configuration of the adapter by using Java 8 lambdas, as the following example shows: + +[source, java] +---- +new MessageListenerAdapter((ReplyingMessageListener) data -> { + ... + return result; +})); +---- + +Starting with version 2.2, the `buildListenerArguments(Object)` has been deprecated and new `buildListenerArguments(Object, Channel, Message)` one has been introduced instead. +The new method helps listener to get `Channel` and `Message` arguments to do more, such as calling `channel.basicReject(long, boolean)` in manual acknowledge mode. +The following listing shows the most basic example: + +[source,java] +---- +public class ExtendedListenerAdapter extends MessageListenerAdapter { + + @Override + protected Object[] buildListenerArguments(Object extractedMessage, Channel channel, Message message) { + return new Object[]{extractedMessage, channel, message}; + } + +} +---- + +Now you could configure `ExtendedListenerAdapter` as same as `MessageListenerAdapter` if you need to receive "`channel`" and "`message`". +Parameters of listener should be set as `buildListenerArguments(Object, Channel, Message)` returned, as the following example of listener shows: + +[source,java] +---- +public void handleMessage(Object object, Channel channel, Message message) throws IOException { + ... +} +---- + +[[container]] +== 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. + +Prior to version 2.0, there was one listener container, the `SimpleMessageListenerContainer`. +There is now a second container, the `DirectMessageListenerContainer`. +The differences between the containers and criteria you might apply when choosing which to use are described in xref:amqp/receiving-messages/choose-container.adoc[Choosing a Container]. + +The following listing shows the most basic example, which works by using the, `SimpleMessageListenerContainer`: + +[source,java] +---- +SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); +container.setConnectionFactory(rabbitConnectionFactory); +container.setQueueNames("some.queue"); +container.setMessageListener(new MessageListenerAdapter(somePojo)); +---- + +As an "`active`" component, it is most common to create the listener container with a bean definition so that it can run in the background. +The following example shows one way to do so with XML: + +[source,xml] +---- + + + +---- + +The following listing shows another way to do so with XML: + +[source,xml] +---- + + + +---- + +Both of the preceding examples create a `DirectMessageListenerContainer` (notice the `type` attribute -- it defaults to `simple`). + +Alternately, you may prefer to use Java configuration, which looks similar to the preceding code snippet: + +[source,java] +---- +@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]] +== Consumer Priority + +Starting with RabbitMQ Version 3.2, the broker now supports consumer priority (see https://www.rabbitmq.com/blog/2013/12/16/using-consumer-priorities-with-rabbitmq/[Using Consumer Priorities with RabbitMQ]). +This is enabled by setting the `x-priority` argument on the consumer. +The `SimpleMessageListenerContainer` now supports setting consumer arguments, as the following example shows: + +[source,java] +---- + +container.setConsumerArguments(Collections. + singletonMap("x-priority", Integer.valueOf(10))); +---- + +For convenience, the namespace provides the `priority` attribute on the `listener` element, as the following example shows: + +[source,xml] +---- + + + +---- + +Starting with version 1.3, you can modify the queues on which the container listens at runtime. +See <>. + +[[lc-auto-delete]] +== `auto-delete` Queues + +When a container is configured to listen to `auto-delete` queues, the queue has an `x-expires` option, or the https://www.rabbitmq.com/ttl.html[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). +Before version 1.3, the container could not be restarted because the queue was missing. +The `RabbitAdmin` only automatically redeclares queues and so on when the connection is closed or when it opens, which does not happen when the container is stopped and started. + +Starting with version 1.3, the container uses a `RabbitAdmin` to redeclare any missing queues during startup. + +You can also use conditional declaration (see xref:amqp/broker-configuration.adoc#conditional-declaration[Conditional Declaration]) together with an `auto-startup="false"` admin to defer queue declaration until the container is started. +The following example shows how to do so: + +[source,xml] +---- + + + + + + + + + + + + + +---- + +In this case, 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. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-returns.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-returns.adoc new file mode 100644 index 0000000000..a48bcd2fea --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-returns.adoc @@ -0,0 +1,24 @@ +[[async-returns]] += Asynchronous `@RabbitListener` Return Types +:page-section-summary-toc: 1 + +`@RabbitListener` (and `@RabbitHandler`) methods can be specified with asynchronous return types `CompletableFuture` and `Mono`, letting the reply be sent asynchronously. +`ListenableFuture` is no longer supported; it has been deprecated by Spring Framework. + +IMPORTANT: 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. + +Starting with version 3.0.5, the `@RabbitListener` (and `@RabbitHandler`) methods can be marked with Kotlin `suspend` and the whole handling process and reply producing (optional) happens on respective Kotlin coroutine. +All the mentioned rules about `AcknowledgeMode.MANUAL` are still apply. +The `org.jetbrains.kotlinx:kotlinx-coroutines-reactor` dependency must be present in classpath to allow `suspend` function invocations. + +Also starting with version 3.0.5, if a `RabbitListenerErrorHandler` is configured on a listener with an async return type (including Kotlin suspend functions), the error handler is invoked after a failure. +See xref:amqp/receiving-messages/async-annotation-driven/error-handling.adoc[Handling Exceptions] for more information about this error handler and its purpose. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/batch.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/batch.adoc new file mode 100644 index 0000000000..80ad49c2b2 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/batch.adoc @@ -0,0 +1,90 @@ +[[receiving-batch]] += @RabbitListener with Batching + +When receiving a xref:amqp/sending-messages.adoc#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` or `Collection`: + +[source, java] +---- +@Bean +public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(connectionFactory()); + factory.setBatchListener(true); + return factory; +} + +@RabbitListener(queues = "batch.1") +public void listen1(List in) { + ... +} + +// or + +@RabbitListener(queues = "batch.2") +public void listen2(List> 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 xref:amqp/receiving-messages/async-annotation-driven/method-selection.adoc[multi-method listener]. + +Also starting with version 2.2. 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. + +[source, java] +---- +@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`: + +[source, java] +---- +@RabbitListener(queues = "batch.1", containerFactory = "consumerBatchContainerFactory") +public void consumerBatch1(List amqpMessages) { + ... +} + +@RabbitListener(queues = "batch.2", containerFactory = "consumerBatchContainerFactory") +public void consumerBatch2(List> messages) { + ... +} + +@RabbitListener(queues = "batch.3", containerFactory = "consumerBatchContainerFactory") +public void consumerBatch3(List strings) { + ... +} +---- + +* the first is called with the raw, unconverted `org.springframework.amqp.core.Message` s received. +* the second is called with the `org.springframework.messaging.Message` s with converted payloads and mapped headers/properties. +* the third is called with the converted payloads, with no access to headers/properties. + +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`. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/choose-container.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/choose-container.adoc new file mode 100644 index 0000000000..78b3a6ccba --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/choose-container.adoc @@ -0,0 +1,36 @@ +[[choose-container]] += Choosing a Container + +Version 2.0 introduced the `DirectMessageListenerContainer` (DMLC). +Previously, only the `SimpleMessageListenerContainer` (SMLC) was available. +The SMLC 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 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 xref:amqp/containerAttributes.adoc[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. +* Threads are shared across consumers rather than having a dedicated thread for each consumer in the SMLC. +However, see the IMPORTANT note about the connection factory configuration in xref:amqp/receiving-messages/threading.adoc[Threading and Asynchronous Consumers]. + +See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration] for information about which configuration properties apply to each container. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/consumer-events.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/consumer-events.adoc new file mode 100644 index 0000000000..af5e756f94 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/consumer-events.adoc @@ -0,0 +1,39 @@ +[[consumer-events]] += Consumer Events + +The containers publish application events whenever a listener +(consumer) experiences a failure of some kind. +The event `ListenerContainerConsumerFailedEvent` has the following properties: + +* `container`: The listener container where the consumer experienced the problem. +* `reason`: A textual reason for the failure. +* `fatal`: A boolean indicating whether the failure was fatal. +With non-fatal exceptions, the container tries to restart the consumer, according to the `recoveryInterval` or `recoveryBackoff` (for the `SimpleMessageListenerContainer`) or the `monitorInterval` (for the `DirectMessageListenerContainer`). +* `throwable`: The `Throwable` that was caught. + +These events can be consumed by implementing `ApplicationListener`. + +NOTE: System-wide events (such as connection failures) are published by all consumers when `concurrentConsumers` is greater than 1. + +If a consumer fails because one if its queues is being used exclusively, by default, as well as publishing the event, a `DEBUG` log is issued (since 3.1, previously WARN). +To change this logging behavior, provide a custom `ConditionalExceptionLogger` in the `AbstractMessageListenerContainer` instance's `exclusiveConsumerExceptionLogger` property. +In addition, the `SimpleMessageListenerContainer` consumer restart after such an exception is now logged at DEBUG level by default (previously INFO). +A new method `logRestart()` has been added to the `ConditionalExceptionLogger` to allow this to be changed. + +Also, the `AbstractMessageListenerContainer.DefaultExclusiveConsumerLogger` is now public, allowing it to be sub classed. + +See also <>. + +Fatal errors are always logged at the `ERROR` level. +This it not modifiable. + +Several other events are published at various stages of the container lifecycle: + +* `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 a `consumeOk` is received from the broker, contains the queue name and `consumerTag` +* `ListenerContainerIdleEvent`: See xref:amqp/receiving-messages/idle-containers.adoc[Detecting Idle Asynchronous Consumers]. +* `MissingQueueEvent`: When a missing queue is detected. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/consumerTags.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/consumerTags.adoc new file mode 100644 index 0000000000..80cfbecabd --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/consumerTags.adoc @@ -0,0 +1,21 @@ +[[consumerTags]] += Consumer Tags +:page-section-summary-toc: 1 + +You can provide a strategy to generate consumer tags. +By default, the consumer tag is generated by the broker. +The following listing shows the `ConsumerTagStrategy` interface definition: + +[source,java] +---- +public interface ConsumerTagStrategy { + + String createConsumerTag(String queue); + +} +---- + +The queue is made available so that it can (optionally) be used in the tag. + +See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration]. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/de-batching.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/de-batching.adoc new file mode 100644 index 0000000000..30562937f0 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/de-batching.adoc @@ -0,0 +1,16 @@ +[[de-batching]] += Batched Messages +:page-section-summary-toc: 1 + +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. +See xref:amqp/sending-messages.adoc#template-batching[Batching] for more information about batching. + +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 xref:amqp/sending-messages.adoc#template-batching[producer created batches] as `List`. +See xref:amqp/receiving-messages/batch.adoc[@RabbitListener with Batching] for information about using this feature with `@RabbitListener`. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/idle-containers.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/idle-containers.adoc new file mode 100644 index 0000000000..239fa97a2b --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/idle-containers.adoc @@ -0,0 +1,95 @@ +[[idle-containers]] += 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. + +Starting with version 1.6, 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. + +To configure this feature, set `idleEventInterval` on the container. +The following example shows how to do so in XML and in Java (for both a `SimpleMessageListenerContainer` and a `SimpleRabbitListenerContainerFactory`): + +[source, xml] +---- + + + +---- + +[source, java] +---- +@Bean +public SimpleMessageListenerContainer(ConnectionFactory connectionFactory) { + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); + ... + container.setIdleEventInterval(60000L); + ... + return container; +} +---- + +[source, java] +---- +@Bean +public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(rabbitConnectionFactory()); + factory.setIdleEventInterval(60000L); + ... + return factory; +} +---- + +In each of these cases, an event is published once per minute while the container is idle. + +[[event-consumption]] +== Event Consumption + +You can capture idle events by implementing `ApplicationListener` -- either a general listener, or one narrowed to only +receive this specific event. +You can also use `@EventListener`, introduced in Spring Framework 4.2. + +The following example combines the `@RabbitListener` and `@EventListener` into a single class. +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 + +The following example shows how to create listeners by using both the `@RabbitListener` and the `@EventListener` annotations: + +[source, Java] +---- +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) { + ... + } + +} +---- + +IMPORTANT: Event listeners see events for all containers. +Consequently, in the preceding example, we narrow the events received based on the listener ID. + +CAUTION: 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. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer-observation.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer-observation.adoc new file mode 100644 index 0000000000..fadad7cccc --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer-observation.adoc @@ -0,0 +1,19 @@ +[[micrometer-observation]] += Micrometer Observation +:page-section-summary-toc: 1 + +Using Micrometer for observation is now supported, since version 3.0, for the `RabbitTemplate` and listener containers. + +Set `observationEnabled` on each component to enable observation; this will disable xref:amqp/receiving-messages/micrometer.adoc[Micrometer Timers] because the timers will now be managed with each observation. +When using annotated listeners, set `observationEnabled` on the container factory. + +Refer to https://micrometer.io/docs/tracing[Micrometer Tracing] for more information. + +To add tags to timers/traces, configure a custom `RabbitTemplateObservationConvention` or `RabbitListenerObservationConvention` to the template or listener container, respectively. + +The default implementations add the `name` tag for template observations and `listener.id` tag for containers. + +You can either subclass `DefaultRabbitTemplateObservationConvention` or `DefaultRabbitListenerObservationConvention` or provide completely new implementations. + +See xref:appendix/micrometer.adoc[Micrometer Observation Documentation] for more details. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer.adoc new file mode 100644 index 0000000000..3715ee6988 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer.adoc @@ -0,0 +1,21 @@ +[[micrometer]] += Monitoring Listener Performance +:page-section-summary-toc: 1 + +Starting with version 2.2, the listener containers will automatically create and update Micrometer `Timer` s for the listener, if `Micrometer` is detected on the class path, and a single `MeterRegistry` is present in the application context (or exactly one is annotated `@Primary`, such as when using Spring Boot). +The timers can be disabled by setting the container property `micrometerEnabled` to `false`. + +Two timers are maintained - one for successful calls to the listener and one for failures. +With a simple `MessageListener`, there is a pair of timers for each configured queue. + +The timers are named `spring.rabbitmq.listener` and have the following tags: + +* `listenerId` : (listener id or container bean name) +* `queue` : (the queue name for a simple listener or list of configured queue names when `consumerBatchEnabled` is `true` - because a batch may contain messages from multiple queues) +* `result` : `success` or `failure` +* `exception` : `none` or `ListenerExecutionFailedException` + +You can add additional tags using the `micrometerTags` container property. + +Also see xref:stream.adoc#stream-micrometer-observation[Micrometer Observation]. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/polling-consumer.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/polling-consumer.adoc new file mode 100644 index 0000000000..893ab1b141 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/polling-consumer.adoc @@ -0,0 +1,102 @@ +[[polling-consumer]] += 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. +Starting with version 1.5, 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. + +CAUTION: 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)`. + +There are four simple `receive` methods available. +As with the `Exchange` on the sending side, there is a method that requires that a default queue property has been set +directly on the template itself, and there is a method that accepts a queue parameter at runtime. +Version 1.6 introduced variants to accept `timeoutMillis` to override `receiveTimeout` on a per-request basis. +The following listing shows the definitions of the four methods: + +[source,java] +---- +Message receive() throws AmqpException; + +Message receive(String queueName) throws AmqpException; + +Message receive(long timeoutMillis) throws AmqpException; + +Message receive(String queueName, long timeoutMillis) throws AmqpException; +---- + +As in the case of sending messages, the `AmqpTemplate` has some convenience methods for receiving POJOs instead of `Message` instances, and implementations provide a way to customize the `MessageConverter` used to create the `Object` returned: +The following listing shows those methods: + +[source,java] +---- +Object receiveAndConvert() throws AmqpException; + +Object receiveAndConvert(String queueName) throws AmqpException; + +Object receiveAndConvert(long timeoutMillis) throws AmqpException; + +Object receiveAndConvert(String queueName, long timeoutMillis) throws AmqpException; +---- + +Starting with version 2.0, there are variants of these methods that take an additional `ParameterizedTypeReference` argument to convert complex types. +The template must be configured with a `SmartMessageConverter`. +See xref:amqp/message-converters.adoc#json-complex[Converting From a `Message` With `RabbitTemplate`] for more information. + +Similar to `sendAndReceive` methods, beginning with version 1.3, the `AmqpTemplate` has several convenience `receiveAndReply` methods for synchronously receiving, processing and replying to messages. +The following listing shows those method definitions: + +[source,java] +---- + boolean receiveAndReply(ReceiveAndReplyCallback callback) + throws AmqpException; + + boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback) + throws AmqpException; + + boolean receiveAndReply(ReceiveAndReplyCallback callback, + String replyExchange, String replyRoutingKey) throws AmqpException; + + boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback, + String replyExchange, String replyRoutingKey) throws AmqpException; + + boolean receiveAndReply(ReceiveAndReplyCallback callback, + ReplyToAddressCallback replyToAddressCallback) throws AmqpException; + + boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback, + ReplyToAddressCallback replyToAddressCallback) throws AmqpException; +---- + +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. + +Automatic message (request and reply) conversion is applied only if the provided callback is not an instance of `ReceiveAndReplyMessageCallback`, which provides a raw message exchange contract. + +The `ReplyToAddressCallback` is useful for cases requiring custom logic to determine the `replyTo` address at runtime against the received message and reply from the `ReceiveAndReplyCallback`. +By default, `replyTo` information in the request message is used to route the reply. + +The following listing shows an example of POJO-based receive and reply: + +[source,java] +---- +boolean received = + this.template.receiveAndReply(ROUTE, new ReceiveAndReplyCallback() { + + public Invoice handle(Order order) { + return processOrder(order); + } + }); +if (received) { + log.info("We received an order!"); +} +---- + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/threading.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/threading.adoc new file mode 100644 index 0000000000..0edb275b17 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/threading.adoc @@ -0,0 +1,28 @@ +[[threading]] += Threading and Asynchronous Consumers + +A number of different threads are involved with 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. +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. + +NOTE: When using the default `SimpleAsyncTaskExecutor`, for the threads the listener is invoked on, the listener container `beanName` is used in the `threadNamePrefix`. +This is useful for log analysis. +We generally recommend always including the thread name in the logging appender configuration. +When a `TaskExecutor` is specifically provided through the `taskExecutor` property on the container, it is used as is, without modification. +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. + +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 this is not configured, the client uses an internal thread pool executor with (at the time of writing) a pool size of `Runtime.getRuntime().availableProcessors() * 2` for each connection. + +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. + +IMPORTANT: 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 default pool size (at the time of writing) is `Runtime.getRuntime().availableProcessors() * 2`. + +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`, as discussed in <>. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/using-container-factories.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/using-container-factories.adoc new file mode 100644 index 0000000000..c2a2f00d0f --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/using-container-factories.adoc @@ -0,0 +1,51 @@ +[[using-container-factories]] += Using Container Factories + +Listener container factories were introduced to support the `@RabbitListener` and registering containers with the `RabbitListenerEndpointRegistry`, as discussed in xref:amqp/receiving-messages/async-annotation-driven/registration.adoc[Programmatic Endpoint Registration]. + +Starting with version 2.1, 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 +* Add the listener after creation + +The following example shows how to use a `SimpleRabbitListenerEndpoint` to create a listener container: + +[source, java] +---- +@Bean +public SimpleMessageListenerContainer factoryCreatedContainerSimpleListener( + SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory) { + SimpleRabbitListenerEndpoint endpoint = new SimpleRabbitListenerEndpoint(); + endpoint.setQueueNames("queue.1"); + endpoint.setMessageListener(message -> { + ... + }); + return rabbitListenerContainerFactory.createListenerContainer(endpoint); +} +---- + +The following example shows how to add the listener after creation: + +[source, java] +---- +@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`. + +These techniques are useful if you wish to create several containers with similar properties or use a pre-configured container factory such as the one provided by Spring Boot auto configuration or both. + +IMPORTANT: Containers created this way are normal `@Bean` instances and are not registered in the `RabbitListenerEndpointRegistry`. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/request-reply.adoc b/src/reference/antora/modules/ROOT/pages/amqp/request-reply.adoc new file mode 100644 index 0000000000..dadca617d5 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/request-reply.adoc @@ -0,0 +1,301 @@ +[[request-reply]] += Request/Reply Messaging + +The `AmqpTemplate` also provides a variety of `sendAndReceive` methods that accept the same argument options that were described earlier for the one-way send operations (`exchange`, `routingKey`, and `Message`). +Those methods are quite useful for request-reply scenarios, since they handle the configuration of the necessary `reply-to` property before sending and can listen for the reply message on an exclusive queue that is created internally for that purpose. + +Similar request-reply methods are also available where the `MessageConverter` is applied to both the request and reply. +Those methods are named `convertSendAndReceive`. +See the https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/core/AmqpTemplate.html[Javadoc of `AmqpTemplate`] for more detail. + +Starting with version 1.5.0, each of the `sendAndReceive` method variants has an overloaded version that takes `CorrelationData`. +Together with a properly configured connection factory, this enables the receipt of publisher confirms for the send side of the operation. +See xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns] and the https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/rabbit/core/RabbitOperations.html[Javadoc for `RabbitOperations`] for more information. + +Starting with version 2.0, there are variants of these methods (`convertSendAndReceiveAsType`) that take an additional `ParameterizedTypeReference` argument to convert complex returned types. +The template must be configured with a `SmartMessageConverter`. +See xref:amqp/message-converters.adoc#json-complex[Converting From a `Message` With `RabbitTemplate`] for more information. + +Starting with version 2.1, you can configure the `RabbitTemplate` with the `noLocalReplyConsumer` option to control a `noLocal` flag for reply consumers. +This is `false` by default. + +[[reply-timeout]] +== Reply Timeout + +By default, the send and receive methods timeout after five seconds and return null. +You can modify this behavior by setting the `replyTimeout` property. +Starting with version 1.5, if you set the `mandatory` property to `true` (or the `mandatory-expression` evaluates to `true` for a particular message), if the message cannot be delivered to a queue, an `AmqpMessageReturnedException` is thrown. +This exception has `returnedMessage`, `replyCode`, and `replyText` properties, as well as the `exchange` and `routingKey` used for the send. + +NOTE: This feature uses publisher returns. +You can enable it by setting `publisherReturns` to `true` on the `CachingConnectionFactory` (see xref:amqp/connections.adoc#cf-pub-conf-ret[Publisher Confirms and Returns]). +Also, you must not have registered your own `ReturnCallback` with the `RabbitTemplate`. + +Starting with version 2.1.2, a `replyTimedOut` method has been added, letting subclasses be informed of the timeout so that they can clean up any retained state. + +Starting with versions 2.0.11 and 2.1.3, when you use the default `DirectReplyToMessageListenerContainer`, you can add an error handler by setting the template's `replyErrorHandler` property. +This error handler is invoked for any failed deliveries, such as late replies and messages received without a correlation header. +The exception passed in is a `ListenerExecutionFailedException`, which has a `failedMessage` property. + +[[direct-reply-to]] +== RabbitMQ Direct reply-to + +IMPORTANT: Starting with version 3.4.0, the RabbitMQ server supports https://www.rabbitmq.com/direct-reply-to.html[direct reply-to]. +This eliminates the main reason for a fixed reply queue (to avoid the need to create a temporary queue for each request). +Starting with Spring AMQP version 1.4.1 direct reply-to is used by default (if supported by the server) instead of creating temporary reply queues. +When no `replyQueue` is provided (or it is set with a name of `amq.rabbitmq.reply-to`), the `RabbitTemplate` automatically detects whether direct reply-to is supported and either uses it or falls back to using a temporary reply queue. +When using direct reply-to, a `reply-listener` is not required and should not be configured. + +Reply listeners are still supported with named queues (other than `amq.rabbitmq.reply-to`), allowing control of reply concurrency and so on. + +Starting with version 1.6, if you wish to use a temporary, exclusive, auto-delete queue for each +reply, set the `useTemporaryReplyQueues` property to `true`. +This property is ignored if you set a `replyAddress`. + +You can change the criteria that dictate whether to use direct reply-to by subclassing `RabbitTemplate` and overriding `useDirectReplyTo()` to check different criteria. +The method is called once only, when the first request is sent. + +Prior to version 2.0, the `RabbitTemplate` created a new consumer for each request and canceled the consumer when the reply was received (or timed out). +Now the template uses a `DirectReplyToMessageListenerContainer` instead, letting the consumers be reused. +The template still takes care of correlating the replies, so there is no danger of a late reply going to a different sender. +If you want to revert to the previous behavior, set the `useDirectReplyToContainer` (`direct-reply-to-container` when using XML configuration) property to false. + +The `AsyncRabbitTemplate` has no such option. +It always used a `DirectReplyToContainer` for replies when direct reply-to is used. + +Starting with version 2.3.7, the template has a new property `useChannelForCorrelation`. +When this is `true`, the server does not have to copy the correlation id from the request message headers to the reply message. +Instead, the channel used to send the request is used to correlate the reply to the request. + +[[message-correlation-with-a-reply-queue]] +== Message Correlation With A Reply Queue + +When using a fixed reply queue (other than `amq.rabbitmq.reply-to`), you must provide correlation data so that replies can be correlated to requests. +See https://www.rabbitmq.com/tutorials/tutorial-six-java.html[RabbitMQ Remote Procedure Call (RPC)]. +By default, the standard `correlationId` property is used to hold the correlation data. +However, if you wish to use a custom property to hold correlation data, you can set the `correlation-key` attribute on the . +Explicitly setting the attribute to `correlationId` is the same as omitting the attribute. +The client and server must use the same header for correlation data. + +NOTE: Spring AMQP version 1.1 used a custom property called `spring_reply_correlation` for this data. +If you wish to revert to this behavior with the current version (perhaps to maintain compatibility with another application using 1.1), you must set the attribute to `spring_reply_correlation`. + +By default, the template generates its own correlation ID (ignoring any user-supplied value). +If you wish to use your own correlation ID, set the `RabbitTemplate` instance's `userCorrelationId` property to `true`. + +IMPORTANT: The correlation ID must be unique to avoid the possibility of a wrong reply being returned for a request. + +[[reply-listener]] +== Reply Listener Container + +When using RabbitMQ versions prior to 3.4.0, a new temporary queue is used for each reply. +However, a single reply queue can be configured on the template, which can be more efficient and also lets you set arguments on that queue. +In this case, however, you must also provide a sub element. +This element provides a listener container for the reply queue, with the template being the listener. +All of the xref:amqp/containerAttributes.adoc[Message Listener Container Configuration] attributes allowed on a are allowed on the element, except for `connection-factory` and `message-converter`, which are inherited from the template's configuration. + +IMPORTANT: If you run multiple instances of your application or use multiple `RabbitTemplate` instances, you *MUST* use a unique reply queue for each. +RabbitMQ has no ability to select messages from a queue, so, if they all use the same queue, each instance would compete for replies and not necessarily receive their own. + +The following example defines a rabbit template with a connection factory: + +[source,xml] +---- + + + +---- + +While the container and template share a connection factory, they do not share a channel. +Therefore, requests and replies are not performed within the same transaction (if transactional). + +NOTE: Prior to version 1.5.0, the `reply-address` attribute was not available. +Replies were always routed by using the default exchange and the `reply-queue` name as the routing key. +This is still the default, but you can now specify the new `reply-address` attribute. +The `reply-address` can contain an address with the form `/` and the reply is routed to the specified exchange and routed to a queue bound with the routing key. +The `reply-address` has precedence over `reply-queue`. +When only `reply-address` is in use, the `` must be configured as a separate `` component. +The `reply-address` and `reply-queue` (or `queues` attribute on the ``) must refer to the same queue logically. + +With this configuration, a `SimpleListenerContainer` is used to receive the replies, with the `RabbitTemplate` being the `MessageListener`. +When defining a template with the `` namespace element, as shown in the preceding example, the parser defines the container and wires in the template as the listener. + +NOTE: When the template does not use a fixed `replyQueue` (or is using direct reply-to -- see xref:amqp/request-reply.adoc#direct-reply-to[RabbitMQ Direct reply-to]), a listener container is not needed. +Direct `reply-to` is the preferred mechanism when using RabbitMQ 3.4.0 or later. + +If you define your `RabbitTemplate` as a `` or use an `@Configuration` class to define it as an `@Bean` or when you create the template programmatically, you need to define and wire up the reply listener container yourself. +If you fail to do this, the template never receives the replies and eventually times out and returns null as the reply to a call to a `sendAndReceive` method. + +Starting with version 1.5, the `RabbitTemplate` detects if it has been +configured as a `MessageListener` to receive replies. +If not, attempts to send and receive messages with a reply address +fail with an `IllegalStateException` (because the replies are never received). + +Further, if a simple `replyAddress` (queue name) is used, the reply listener container verifies that it is listening +to a queue with the same name. +This check cannot be performed if the reply address is an exchange and routing key and a debug log message is written. + +IMPORTANT: When wiring the reply listener and template yourself, it is important to ensure that the template's `replyAddress` and the container's `queues` (or `queueNames`) properties refer to the same queue. +The template inserts the reply address into the outbound message `replyTo` property. + +The following listing shows examples of how to manually wire up the beans: + +[source,xml] +---- + + + + + + + + + + + + + + + + +---- + +[source,java] +---- + @Bean + public RabbitTemplate amqpTemplate() { + RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory()); + rabbitTemplate.setMessageConverter(msgConv()); + rabbitTemplate.setReplyAddress(replyQueue().getName()); + rabbitTemplate.setReplyTimeout(60000); + rabbitTemplate.setUseDirectReplyToContainer(false); + return rabbitTemplate; + } + + @Bean + public SimpleMessageListenerContainer replyListenerContainer() { + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); + container.setConnectionFactory(connectionFactory()); + container.setQueues(replyQueue()); + container.setMessageListener(amqpTemplate()); + return container; + } + + @Bean + public Queue replyQueue() { + return new Queue("my.reply.queue"); + } +---- + +A complete example of a `RabbitTemplate` wired with a fixed reply queue, together with a "`remote`" listener container that handles the request and returns the reply is shown in https://github.com/spring-projects/spring-amqp/tree/main/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/JavaConfigFixedReplyQueueTests.java[this test case]. + +IMPORTANT: When the reply times out (`replyTimeout`), the `sendAndReceive()` methods return null. + +Prior to version 1.3.6, late replies for timed out messages were only logged. +Now, if a late reply is received, it is rejected (the template throws an `AmqpRejectAndDontRequeueException`). +If the reply queue is configured to send rejected messages to a dead letter exchange, the reply can be retrieved for later analysis. +To do so, bind a queue to the configured dead letter exchange with a routing key equal to the reply queue's name. + +See the https://www.rabbitmq.com/dlx.html[RabbitMQ Dead Letter Documentation] for more information about configuring dead lettering. +You can also take a look at the `FixedReplyQueueDeadLetterTests` test case for an example. + +[[async-template]] +== Async Rabbit Template + +Version 1.6 introduced the `AsyncRabbitTemplate`. +This has similar `sendAndReceive` (and `convertSendAndReceive`) methods to those on the xref:amqp/template.adoc[`AmqpTemplate`]. +However, instead of blocking, they return a `CompletableFuture`. + +The `sendAndReceive` methods return a `RabbitMessageFuture`. +The `convertSendAndReceive` methods return a `RabbitConverterFuture`. + +You can either synchronously retrieve the result later, by invoking `get()` on the future, or you can register a callback that is called asynchronously with the result. +The following listing shows both approaches: + +[source, java] +---- +@Autowired +private AsyncRabbitTemplate template; + +... + +public void doSomeWorkAndGetResultLater() { + + ... + + CompletableFuture future = this.template.convertSendAndReceive("foo"); + + // do some more work + + String reply = null; + try { + reply = future.get(10, TimeUnit.SECONDS); + } + catch (ExecutionException e) { + ... + } + + ... + +} + +public void doSomeWorkAndGetResultAsync() { + + ... + + RabbitConverterFuture future = this.template.convertSendAndReceive("foo"); + future.whenComplete((result, ex) -> { + if (ex == null) { + // success + } + else { + // failure + } + }); + + ... + +} +---- + +If `mandatory` is set and the message cannot be delivered, the future throws an `ExecutionException` with a cause of `AmqpMessageReturnedException`, which encapsulates the returned message and information about the return. + +If `enableConfirms` is set, the future has a property called `confirm`, which is itself a `CompletableFuture` with `true` indicating a successful publish. +If the confirm future is `false`, the `RabbitFuture` has a further property called `nackCause`, which contains the reason for the failure, if available. + +IMPORTANT: The publisher confirm is discarded if it is received after the reply, since the reply implies a successful publish. + +You can set the `receiveTimeout` property on the template to time out replies (it defaults to `30000` - 30 seconds). +If a timeout occurs, the future is completed with an `AmqpReplyTimeoutException`. + +The template implements `SmartLifecycle`. +Stopping the template while there are pending replies causes the pending `Future` instances to be canceled. + +Starting with version 2.0, the asynchronous template now supports https://www.rabbitmq.com/direct-reply-to.html[direct reply-to] instead of a configured reply queue. +To enable this feature, use one of the following constructors: + +[source, java] +---- +public AsyncRabbitTemplate(ConnectionFactory connectionFactory, String exchange, String routingKey) + +public AsyncRabbitTemplate(RabbitTemplate template) +---- + +See xref:amqp/request-reply.adoc#direct-reply-to[RabbitMQ Direct reply-to] to use direct reply-to with the synchronous `RabbitTemplate`. + +Version 2.0 introduced variants of these methods (`convertSendAndReceiveAsType`) that take an additional `ParameterizedTypeReference` argument to convert complex returned types. +You must configure the underlying `RabbitTemplate` with a `SmartMessageConverter`. +See xref:amqp/message-converters.adoc#json-complex[Converting From a `Message` With `RabbitTemplate`] for more information. + +IMPORTANT: Starting with version 3.0, the `AsyncRabbitTemplate` methods now return `CompletableFuture` s instead of `ListenableFuture` s. + +[[remoting]] +== Spring Remoting with AMQP + +Spring remoting is no longer supported because the functionality has been removed from Spring Framework. + +Use `sendAndReceive` operations using the `RabbitTemplate` (client side ) and `@RabbitListener` instead. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/resilience-recovering-from-errors-and-broker-failures.adoc b/src/reference/antora/modules/ROOT/pages/amqp/resilience-recovering-from-errors-and-broker-failures.adoc new file mode 100644 index 0000000000..18f5107668 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/resilience-recovering-from-errors-and-broker-failures.adoc @@ -0,0 +1,212 @@ +[[resilience:-recovering-from-errors-and-broker-failures]] += Resilience: Recovering from Errors and Broker Failures + +Some of the key (and most popular) high-level features that Spring AMQP provides are to do with recovery and automatic re-connection in the event of a protocol error or broker failure. +We have seen all the relevant components already in this guide, but it should help to bring them all together here and call out the features and recovery scenarios individually. + +The primary reconnection features are enabled by the `CachingConnectionFactory` itself. +It is also often beneficial to use the `RabbitAdmin` auto-declaration features. +In addition, if you care about guaranteed delivery, you probably also need to use the `channelTransacted` flag in `RabbitTemplate` and `SimpleMessageListenerContainer` and the `AcknowledgeMode.AUTO` (or manual if you do the acks yourself) in the `SimpleMessageListenerContainer`. + +[[automatic-declaration]] +== Automatic Declaration of Exchanges, Queues, and Bindings + +The `RabbitAdmin` component can declare exchanges, queues, and bindings on startup. +It does this lazily, through a `ConnectionListener`. +Consequently, if the broker is not present on startup, it does not matter. +The first time a `Connection` is used (for example, +by sending a message) the listener fires and the admin features is applied. +A further benefit of doing the auto declarations in a listener is that, if the connection is dropped for any reason (for example, +broker death, network glitch, and others), they are applied again when the connection is re-established. + +NOTE: Queues declared this way must have fixed names -- either explicitly declared or generated by the framework for `AnonymousQueue` instances. +Anonymous queues are non-durable, exclusive, and auto-deleting. + +IMPORTANT: Automatic declaration is performed only when the `CachingConnectionFactory` cache mode is `CHANNEL` (the default). +This limitation exists because exclusive and auto-delete queues are bound to the connection. + +Starting with version 2.2.2, the `RabbitAdmin` will detect beans of type `DeclarableCustomizer` and apply the function before actually processing the declaration. +This is useful, for example, to set a new argument (property) before it has first class support within the framework. + +[source, java] +---- +@Bean +public DeclarableCustomizer customizer() { + return dec -> { + if (dec instanceof Queue && ((Queue) dec).getName().equals("my.queue")) { + dec.addArgument("some.new.queue.argument", true); + } + return dec; + }; +} +---- + +It is also useful in projects that don't provide direct access to the `Declarable` bean definitions. + +See also xref:amqp/connections.adoc#auto-recovery[RabbitMQ Automatic Connection/Topology recovery]. + +[[retry]] +== Failures in Synchronous Operations and Options for Retry + +If you lose your connection to the broker in a synchronous sequence when using `RabbitTemplate` (for instance), Spring AMQP throws an `AmqpException` (usually, but not always, `AmqpIOException`). +We do not try to hide the fact that there was a problem, so you have to be able to catch and respond to the exception. +The easiest thing to do if you suspect that the connection was lost (and it was not your fault) is to try the operation again. +You can do this manually, or you could look at using Spring Retry to handle the retry (imperatively or declaratively). + +Spring Retry provides a couple of AOP interceptors and a great deal of flexibility to specify the parameters of the retry (number of attempts, exception types, backoff algorithm, and others). +Spring AMQP also provides some convenience factory beans for creating Spring Retry interceptors in a convenient form for AMQP use cases, with strongly typed callback interfaces that you can use to implement custom recovery logic. +See the Javadoc and properties of `StatefulRetryOperationsInterceptor` and `StatelessRetryOperationsInterceptor` for more detail. +Stateless retry is appropriate if there is no transaction or if a transaction is started inside the retry callback. +Note that stateless retry is simpler to configure and analyze than stateful retry, but it is not usually appropriate if there is an ongoing transaction that must be rolled back or definitely is going to roll back. +A dropped connection in the middle of a transaction should have the same effect as a rollback. +Consequently, for reconnections where the transaction is started higher up the stack, stateful retry is usually the best choice. +Stateful retry needs a mechanism to uniquely identify a message. +The simplest approach is to have the sender put a unique value in the `MessageId` message property. +The provided message converters provide an option to do this: you can set `createMessageIds` to `true`. +Otherwise, you can inject a `MessageKeyGenerator` implementation into the interceptor. +The key generator must return a unique key for each message. +In versions prior to version 2.0, a `MissingMessageIdAdvice` was provided. +It enabled messages without a `messageId` property to be retried exactly once (ignoring the retry settings). +This advice is no longer provided, since, along with `spring-retry` version 1.2, its functionality is built into the interceptor and message listener containers. + +NOTE: For backwards compatibility, a message with a null message ID is considered fatal for the consumer (consumer is stopped) by default (after one retry). +To replicate the functionality provided by the `MissingMessageIdAdvice`, you can set the `statefulRetryFatalWithNullMessageId` property to `false` on the listener container. +With that setting, the consumer continues to run and the message is rejected (after one retry). +It is discarded or routed to the dead letter queue (if one is configured). + +Starting with version 1.3, a builder API is provided to aid in assembling these interceptors by using Java (in `@Configuration` classes). +The following example shows how to do so: + +[source,java] +---- +@Bean +public StatefulRetryOperationsInterceptor interceptor() { + return RetryInterceptorBuilder.stateful() + .maxAttempts(5) + .backOffOptions(1000, 2.0, 10000) // initialInterval, multiplier, maxInterval + .build(); +} +---- + +Only a subset of retry capabilities can be configured this way. +More advanced features would need the configuration of a `RetryTemplate` as a Spring bean. +See the https://docs.spring.io/spring-retry/docs/api/current/[Spring Retry Javadoc] for complete information about available policies and their configuration. + +[[batch-retry]] +== Retry with Batch Listeners + +It is not recommended to configure retry with a batch listener, unless the batch was created by the producer, in a single record. +See xref:amqp/receiving-messages/de-batching.adoc[Batched Messages] for information about consumer and producer-created batches. +With a consumer-created batch, the framework has no knowledge about which message in the batch caused the failure so recovery after the retries are exhausted is not possible. +With producer-created batches, since there is only one message that actually failed, the whole message can be recovered. +Applications may want to inform a custom recoverer where in the batch the failure occurred, perhaps by setting an index property of the thrown exception. + +A retry recoverer for a batch listener must implement `MessageBatchRecoverer`. + +[[async-listeners]] +== Message Listeners and the Asynchronous Case + +If a `MessageListener` fails because of a business exception, the exception is handled by the message listener container, which then goes back to listening for another message. +If the failure is caused by a dropped connection (not a business exception), the consumer that is collecting messages for the listener has to be cancelled and restarted. +The `SimpleMessageListenerContainer` handles this seamlessly, and it leaves a log to say that the listener is being restarted. +In fact, it loops endlessly, trying to restart the consumer. +Only if the consumer is very badly behaved indeed will it give up. +One side effect is that if the broker is down when the container starts, it keeps trying until a connection can be established. + +Business exception handling, as opposed to protocol errors and dropped connections, might need more thought and some custom configuration, especially if transactions or container acks are in use. +Prior to 2.8.x, RabbitMQ had no definition of dead letter behavior. +Consequently, by default, a message that is rejected or rolled back because of a business exception can be redelivered endlessly. +To put a limit on the client on the number of re-deliveries, one choice is a `StatefulRetryOperationsInterceptor` in the advice chain of the listener. +The interceptor can have a recovery callback that implements a custom dead letter action -- whatever is appropriate for your particular environment. + +Another alternative is to set the container's `defaultRequeueRejected` property to `false`. +This causes all failed messages to be discarded. +When using RabbitMQ 2.8.x or higher, this also facilitates delivering the message to a dead letter exchange. + +Alternatively, you can throw a `AmqpRejectAndDontRequeueException`. +Doing so prevents message requeuing, regardless of the setting of the `defaultRequeueRejected` property. + +Starting with version 2.1, an `ImmediateRequeueAmqpException` is introduced to perform exactly the opposite logic: the message will be requeued, regardless of the setting of the `defaultRequeueRejected` property. + +Often, a combination of both techniques is used. +You can use a `StatefulRetryOperationsInterceptor` in the advice chain with a `MessageRecoverer` that throws an `AmqpRejectAndDontRequeueException`. +The `MessageRecover` is called when all retries have been exhausted. +The `RejectAndDontRequeueRecoverer` does exactly that. +The default `MessageRecoverer` consumes the errant message and emits a `WARN` message. + +Starting with version 1.3, a new `RepublishMessageRecoverer` is provided, to allow publishing of failed messages after retries are exhausted. + +When a recoverer consumes the final exception, the message is ack'd and is not sent to the dead letter exchange by the broker, if configured. + +NOTE: When `RepublishMessageRecoverer` is used on the consumer side, the received message has `deliveryMode` in the `receivedDeliveryMode` message property. +In this case the `deliveryMode` is `null`. +That means a `NON_PERSISTENT` delivery mode on the broker. +Starting with version 2.0, you can configure the `RepublishMessageRecoverer` for the `deliveryMode` to set into the message to republish if it is `null`. +By default, it uses `MessageProperties` default value - `MessageDeliveryMode.PERSISTENT`. + +The following example shows how to set a `RepublishMessageRecoverer` as the recoverer: + +[source,java] +---- +@Bean +RetryOperationsInterceptor interceptor() { + return RetryInterceptorBuilder.stateless() + .maxAttempts(5) + .recoverer(new RepublishMessageRecoverer(amqpTemplate(), "something", "somethingelse")) + .build(); +} +---- + +The `RepublishMessageRecoverer` publishes the message with additional information in message headers, such as the exception message, stack trace, original exchange, and routing key. +Additional headers can be added by creating a subclass and overriding `additionalHeaders()`. +The `deliveryMode` (or any other properties) can also be changed in the `additionalHeaders()`, as the following example shows: + +[source,java] +---- +RepublishMessageRecoverer recoverer = new RepublishMessageRecoverer(amqpTemplate, "error") { + + protected Map additionalHeaders(Message message, Throwable cause) { + message.getMessageProperties() + .setDeliveryMode(message.getMessageProperties().getReceivedDeliveryMode()); + return null; + } + +}; +---- + +Starting with version 2.0.5, the stack trace may be truncated if it is too large; this is because all headers have to fit in a single frame. +By default, if the stack trace would cause less than 20,000 bytes ('headroom') to be available for other headers, it will be truncated. +This can be adjusted by setting the recoverer's `frameMaxHeadroom` property, if you need more or less space for other headers. +Starting with versions 2.1.13, 2.2.3, the exception message is included in this calculation, and the amount of stack trace will be maximized using the following algorithm: + +* if the stack trace alone would exceed the limit, the exception message header will be truncated to 97 bytes plus `...` and the stack trace is truncated too. +* if the stack trace is small, the message will be truncated (plus `...`) to fit in the available bytes (but the message within the stack trace itself is truncated to 97 bytes plus `...`). + +Whenever a truncation of any kind occurs, the original exception will be logged to retain the complete information. +The evaluation is performed after the headers are enhanced so information such as the exception type can be used in the expressions. + +Starting with version 2.4.8, the error exchange and routing key can be provided as SpEL expressions, with the `Message` being the root object for the evaluation. + +Starting with version 2.3.3, a new subclass `RepublishMessageRecovererWithConfirms` is provided; this supports both styles of publisher confirms and will wait for the confirmation before returning (or throw an exception if not confirmed or the message is returned). + +If the confirm type is `CORRELATED`, the subclass will also detect if a message is returned and throw an `AmqpMessageReturnedException`; if the publication is negatively acknowledged, it will throw an `AmqpNackReceivedException`. + +If the confirm type is `SIMPLE`, the subclass will invoke the `waitForConfirmsOrDie` method on the channel. + +See xref:amqp/connections.adoc#cf-pub-conf-ret[Publisher Confirms and Returns] for more information about confirms and returns. + +Starting with version 2.1, an `ImmediateRequeueMessageRecoverer` is added to throw an `ImmediateRequeueAmqpException`, which notifies a listener container to requeue the current failed message. + +[[exception-classification-for-spring-retry]] +== Exception Classification for Spring Retry + +Spring Retry has a great deal of flexibility for determining which exceptions can invoke retry. +The default configuration retries for all exceptions. +Given that user exceptions are wrapped in a `ListenerExecutionFailedException`, we need to ensure that the classification examines the exception causes. +The default classifier looks only at the top level exception. + +Since Spring Retry 1.0.3, the `BinaryExceptionClassifier` has a property called `traverseCauses` (default: `false`). +When `true`, it travers exception causes until it finds a match or there is no cause. + +To use this classifier for retry, you can use a `SimpleRetryPolicy` created with the constructor that takes the max attempts, the `Map` of `Exception` instances, and the boolean (`traverseCauses`) and inject this policy into the `RetryTemplate`. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/sending-messages.adoc b/src/reference/antora/modules/ROOT/pages/amqp/sending-messages.adoc new file mode 100644 index 0000000000..64188a386f --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/sending-messages.adoc @@ -0,0 +1,224 @@ +[[sending-messages]] += Sending Messages + +When sending a message, you can use any of the following methods: + +[source,java] +---- +void send(Message message) throws AmqpException; + +void send(String routingKey, Message message) throws AmqpException; + +void send(String exchange, String routingKey, Message message) throws AmqpException; +---- + +We can begin our discussion with the last method in the preceding listing, since it is actually the most explicit. +It lets an AMQP exchange name (along with a routing key)be provided at runtime. +The last parameter is the callback that is responsible for actual creating the message instance. +An example of using this method to send a message might look like this: +The following example shows how to use the `send` method to send a message: + +[source,java] +---- +amqpTemplate.send("marketData.topic", "quotes.nasdaq.THING1", + new Message("12.34".getBytes(), someProperties)); +---- + +You can set the `exchange` property on the template itself if you plan to use that template instance to send to the same exchange most or all of the time. +In such cases, you can use the second method in the preceding listing. +The following example is functionally equivalent to the previous example: + +[source,java] +---- +amqpTemplate.setExchange("marketData.topic"); +amqpTemplate.send("quotes.nasdaq.FOO", new Message("12.34".getBytes(), someProperties)); +---- + +If both the `exchange` and `routingKey` properties are set on the template, you can use the method that accepts only the `Message`. +The following example shows how to do so: + +[source,java] +---- +amqpTemplate.setExchange("marketData.topic"); +amqpTemplate.setRoutingKey("quotes.nasdaq.FOO"); +amqpTemplate.send(new Message("12.34".getBytes(), someProperties)); +---- + +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. +In both cases, the default is an empty `String`, but that is actually a sensible default. +As far as the routing key is concerned, it is not always necessary in the first place (for example, for +a `Fanout` exchange). +Furthermore, a queue may be bound to an exchange with an empty `String`. +Those are both legitimate scenarios for reliance on the default empty `String` value for the routing key property of the template. +As far as the exchange name is concerned, the empty `String` is commonly used because the AMQP specification defines the "`default exchange`" as having no name. +Since all queues are automatically bound to that default exchange (which is a direct exchange), using their name as the binding value, the second method in the preceding listing can be used for simple point-to-point messaging to any queue through the default exchange. +You can provide the queue name as the `routingKey`, either by providing the method parameter at runtime. +The following example shows how to do so: + +[source,java] +---- +RabbitTemplate template = new RabbitTemplate(); // using default no-name Exchange +template.send("queue.helloWorld", new Message("Hello World".getBytes(), someProperties)); +---- + +Alternately, you can create a template that can be used for publishing primarily or exclusively to a single Queue. +The following example shows how to do so: + +[source,java] +---- +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]] +== Message Builder API + +Starting with version 1.3, a message builder API is provided by the `MessageBuilder` and `MessagePropertiesBuilder`. +These methods provide a convenient "`fluent`" means of creating a message or message properties. +The following examples show the fluent API in action: + +[source,java] +---- +Message message = MessageBuilder.withBody("foo".getBytes()) + .setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN) + .setMessageId("123") + .setHeader("bar", "baz") + .build(); +---- + +[source,java] +---- +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(); +---- + +Each of the properties defined on the https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/core/MessageProperties.html[`MessageProperties`] can be set. +Other methods include `setHeader(String key, String value)`, `removeHeader(String key)`, `removeHeaders()`, and `copyProperties(MessageProperties properties)`. +Each property setting method has a `set*IfAbsent()` variant. +In the cases where a default initial value exists, the method is named `set*IfAbsentOrDefault()`. + +Five static methods are provided to create an initial message builder: + +[source,java] +---- +public static MessageBuilder withBody(byte[] body) <1> + +public static MessageBuilder withClonedBody(byte[] body) <2> + +public static MessageBuilder withBody(byte[] body, int from, int to) <3> + +public static MessageBuilder fromMessage(Message message) <4> + +public static MessageBuilder fromClonedMessage(Message message) <5> +---- + +<1> The message created by the builder has a body that is a direct reference to the argument. +<2> The message created by the builder has a body that is a new array containing a copy of bytes in the argument. +<3> The message created by the builder has a body that is a new array containing the range of bytes from the argument. +See https://docs.oracle.com/javase/7/docs/api/java/util/Arrays.html[`Arrays.copyOfRange()`] for more details. +<4> The message created by the builder has a body that is a direct reference to the body of the argument. +The argument's properties are copied to a new `MessageProperties` object. +<5> The message created by the builder has a body that is a new array containing a copy of the argument's body. +The argument's properties are copied to a new `MessageProperties` object. + +Three static methods are provided to create a `MessagePropertiesBuilder` instance: + +[source,java] +---- +public static MessagePropertiesBuilder newInstance() <1> + +public static MessagePropertiesBuilder fromProperties(MessageProperties properties) <2> + +public static MessagePropertiesBuilder fromClonedProperties(MessageProperties properties) <3> +---- + +<1> A new message properties object is initialized with default values. +<2> The builder is initialized with, and `build()` will return, the provided properties object., +<3> The argument's properties are copied to a new `MessageProperties` object. + +With the `RabbitTemplate` implementation of `AmqpTemplate`, each of the `send()` methods has an overloaded version that takes an additional `CorrelationData` object. +When publisher confirms are enabled, this object is returned in the callback described in xref:amqp/template.adoc[`AmqpTemplate`]. +This lets the sender correlate a confirm (`ack` or `nack`) with the sent message. + +Starting with version 1.6.7, the `CorrelationAwareMessagePostProcessor` interface was introduced, allowing the correlation data to be modified after the message has been converted. +The following example shows how to use it: + +[source, java] +---- +Message postProcessMessage(Message message, Correlation correlation); +---- + +In version 2.0, this interface is deprecated. +The method has been moved to `MessagePostProcessor` with a default implementation that delegates to `postProcessMessage(Message message)`. + +Also starting with version 1.6.7, a new callback interface called `CorrelationDataPostProcessor` is provided. +This is invoked after all `MessagePostProcessor` instances (provided in the `send()` method as well as those provided in `setBeforePublishPostProcessors()`). +Implementations can update or replace the correlation data supplied in the `send()` method (if any). +The `Message` and original `CorrelationData` (if any) are provided as arguments. +The following example shows how to use the `postProcess` method: + +[source, java] +---- +CorrelationData postProcess(Message message, CorrelationData correlationData); +---- + +[[publisher-returns]] +== Publisher Returns + +When the template's `mandatory` property is `true`, returned messages are provided by the callback described in xref:amqp/template.adoc[`AmqpTemplate`]. + +Starting with version 1.4, the `RabbitTemplate` supports the SpEL `mandatoryExpression` property, which is evaluated against each request message as the root evaluation object, resolving to a `boolean` value. +Bean references, such as `@myBean.isMandatory(#root)`, can be used in the expression. + +Publisher returns can also be used internally by the `RabbitTemplate` in send and receive operations. +See xref:amqp/request-reply.adoc#reply-timeout[Reply Timeout] for more information. + +[[template-batching]] +== Batching + +Version 1.4.2 introduced the `BatchingRabbitTemplate`. +This is a subclass of `RabbitTemplate` with an overridden `send` method that batches messages according to the `BatchingStrategy`. +Only when a batch is complete is the message sent to RabbitMQ. +The following listing shows the `BatchingStrategy` interface definition: + +[source, java] +---- +public interface BatchingStrategy { + + MessageBatch addToBatch(String exchange, String routingKey, Message message); + + Date nextRelease(); + + Collection releaseBatches(); + +} +---- + +CAUTION: Batched data is held in memory. +Unsent messages can be lost in the event of a system failure. + +A `SimpleBatchingStrategy` is provided. +It supports sending messages to a single exchange or routing key. +It has the following properties: + +* `batchSize`: The number of messages in a batch before it is sent. +* `bufferLimit`: The maximum size of the batched message. +This preempts the `batchSize`, if exceeded, and causes a partial batch to be sent. +* `timeout`: A time after which a partial batch is sent when there is no new activity adding messages to the batch. + +The `SimpleBatchingStrategy` formats the batch by preceding each embedded message with a four-byte binary length. +This is communicated to the receiving system by setting the `springBatchFormat` message property to `lengthHeader4`. + +IMPORTANT: 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. + +However, see xref:amqp/receiving-messages/batch.adoc[@RabbitListener with Batching] for more information. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/template.adoc b/src/reference/antora/modules/ROOT/pages/amqp/template.adoc new file mode 100644 index 0000000000..2973335b86 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/template.adoc @@ -0,0 +1,547 @@ +[[amqp-template]] += `AmqpTemplate` + +As with many other high-level abstractions provided by the Spring Framework and related projects, Spring AMQP provides a "`template`" that plays a central role. +The interface that defines the main operations is called `AmqpTemplate`. +Those operations cover the general behavior for sending and receiving messages. +In other words, they are not unique to any implementation -- hence the "`AMQP`" in the name. +On the other hand, there are implementations of that interface that are tied to implementations of the AMQP protocol. +Unlike JMS, which is an interface-level API itself, AMQP is a wire-level protocol. +The implementations of that protocol provide their own client libraries, so each implementation of the template interface depends on a particular client library. +Currently, there is only a single implementation: `RabbitTemplate`. +In the examples that follow, we often use an `AmqpTemplate`. +However, when you look at the configuration examples or any code excerpts where the template is instantiated or setters are invoked, you can see the implementation type (for example, `RabbitTemplate`). + +As mentioned earlier, the `AmqpTemplate` interface defines all of the basic operations for sending and receiving messages. +We will explore message sending and reception, respectively, in <> and <>. + +See also xref:amqp/request-reply.adoc#async-template[Async Rabbit Template]. + +[[template-retry]] +== Adding Retry Capabilities + +Starting with version 1.3, you can now configure the `RabbitTemplate` to use a `RetryTemplate` to help with handling problems with broker connectivity. +See the https://github.com/spring-projects/spring-retry[spring-retry] project for complete information. +The following is only one example that uses an exponential back off policy and the default `SimpleRetryPolicy`, which makes three tries before throwing the exception to the caller. + +The following example uses the XML namespace: + +[source,xml] +---- + + + + + + + + + + + +---- + +The following example uses the `@Configuration` annotation in Java: + +[source,java] +---- +@Bean +public RabbitTemplate 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; +} +---- + +Starting with version 1.4, in addition to the `retryTemplate` property, the `recoveryCallback` option is supported on the `RabbitTemplate`. +It is used as a second argument for the `RetryTemplate.execute(RetryCallback retryCallback, RecoveryCallback recoveryCallback)`. + +NOTE: The `RecoveryCallback` is somewhat limited, in that the retry context contains only the `lastThrowable` field. +For more sophisticated use cases, you should use an external `RetryTemplate` so that you can convey additional information to the `RecoveryCallback` through the context's attributes. +The following example shows how to do so: + +[source,java] +---- +retryTemplate.execute( + new RetryCallback() { + + @Override + public Object doWithRetry(RetryContext context) throws Exception { + context.setAttribute("message", message); + return rabbitTemplate.convertAndSend(exchange, routingKey, message); + } + + }, new RecoveryCallback() { + + @Override + public Object recover(RetryContext context) throws Exception { + Object message = context.getAttribute("message"); + Throwable t = context.getLastThrowable(); + // Do something with message + return null; + } + }); +} +---- + +In this case, you would *not* inject a `RetryTemplate` into the `RabbitTemplate`. + +[[publishing-is-async]] +== Publishing is Asynchronous -- How to Detect Successes and Failures + +Publishing messages is an asynchronous mechanism and, by default, messages that cannot be routed are dropped by RabbitMQ. +For successful publishing, you can receive an asynchronous confirm, as described in xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns]. +Consider two failure scenarios: + +* Publish to an exchange but there is no matching destination queue. +* Publish to a non-existent exchange. + +The first case is covered by publisher returns, as described in xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns]. + +For the second case, the message is dropped and no return is generated. +The underlying channel is closed with an exception. +By default, this exception is logged, but you can register a `ChannelListener` with the `CachingConnectionFactory` to obtain notifications of such events. +The following example shows how to add a `ConnectionListener`: + +[source, java] +---- +this.connectionFactory.addConnectionListener(new ConnectionListener() { + + @Override + public void onCreate(Connection connection) { + } + + @Override + public void onShutDown(ShutdownSignalException signal) { + ... + } + +}); +---- + +You can examine the signal's `reason` property to determine the problem that occurred. + +To detect the exception on the sending thread, you can `setChannelTransacted(true)` on the `RabbitTemplate` and the exception is detected on the `txCommit()`. +However, *transactions significantly impede performance*, so consider this carefully before enabling transactions for just this one use case. + +[[template-confirms]] +== Correlated Publisher Confirms and Returns + +The `RabbitTemplate` implementation of `AmqpTemplate` supports publisher confirms and returns. + +For returned messages, the template's `mandatory` property must be set to `true` or the `mandatory-expression` +must evaluate to `true` for a particular message. +This feature requires a `CachingConnectionFactory` that has its `publisherReturns` property set to `true` (see xref:amqp/connections.adoc#cf-pub-conf-ret[Publisher Confirms and Returns]). +Returns are sent to the client by it registering a `RabbitTemplate.ReturnsCallback` by calling `setReturnsCallback(ReturnsCallback callback)`. +The callback must implement the following method: + +[source,java] +---- +void returnedMessage(ReturnedMessage returned); +---- + +The `ReturnedMessage` has the following properties: + +- `message` - the returned message itself +- `replyCode` - a code indicating the reason for the return +- `replyText` - a textual reason for the return - e.g. `NO_ROUTE` +- `exchange` - the exchange to which the message was sent +- `routingKey` - the routing key that was used + +Only one `ReturnsCallback` is supported by each `RabbitTemplate`. +See also xref:amqp/request-reply.adoc#reply-timeout[Reply Timeout]. + +For publisher confirms (also known as publisher acknowledgements), the template requires a `CachingConnectionFactory` that has its `publisherConfirm` property set to `ConfirmType.CORRELATED`. +Confirms are sent to the client by it registering a `RabbitTemplate.ConfirmCallback` by calling `setConfirmCallback(ConfirmCallback callback)`. +The callback must implement this method: + +[source,java] +---- +void confirm(CorrelationData correlationData, boolean ack, String cause); +---- + +The `CorrelationData` is an object supplied by the client when sending the original message. +The `ack` is true for an `ack` and false for a `nack`. +For `nack` instances, the cause may contain a reason for the `nack`, if it is available when the `nack` is generated. +An example is when sending a message to a non-existent exchange. +In that case, the broker closes the channel. +The reason for the closure is included in the `cause`. +The `cause` was added in version 1.4. + +Only one `ConfirmCallback` is supported by a `RabbitTemplate`. + +NOTE: When a rabbit template send operation completes, the channel is closed. +This precludes the reception of confirms or returns when the connection factory cache is full (when there is space in the cache, the channel is not physically closed and the returns and confirms proceed normally). +When the cache is full, the framework defers the close for up to five seconds, in order to allow time for the confirms and returns to be received. +When using confirms, the channel is closed when the last confirm is received. +When using only returns, the channel remains open for the full five seconds. +We generally recommend setting the connection factory's `channelCacheSize` to a large enough value so that the channel on which a message is published is returned to the cache instead of being closed. +You can monitor channel usage by using the RabbitMQ management plugin. +If you see channels being opened and closed rapidly, you should consider increasing the cache size to reduce overhead on the server. + +IMPORTANT: Before version 2.1, channels enabled for publisher confirms were returned to the cache before the confirms were received. +Some other process could check out the channel and perform some operation that causes the channel to close -- such as publishing a message to a non-existent exchange. +This could cause the confirm to be lost. +Version 2.1 and later no longer return the channel to the cache while confirms are outstanding. +The `RabbitTemplate` performs a logical `close()` on the channel after each operation. +In general, this means that only one confirm is outstanding on a channel at a time. + +NOTE: Starting with version 2.2, the callbacks are invoked on one of the connection factory's `executor` threads. +This is to avoid a potential deadlock if you perform Rabbit operations from within the callback. +With previous versions, the callbacks were invoked directly on the `amqp-client` connection I/O thread; this would deadlock if you perform some RPC operation (such as opening a new channel) since the I/O thread blocks waiting for the result, but the result needs to be processed by the I/O thread itself. +With those versions, it was necessary to hand off work (such as sending a messasge) to another thread within the callback. +This is no longer necessary since the framework now hands off the callback invocation to the executor. + +IMPORTANT: The guarantee of receiving a returned message before the ack is still maintained as long as the return callback executes in 60 seconds or less. +The confirm is scheduled to be delivered after the return callback exits or after 60 seconds, whichever comes first. + +The `CorrelationData` object has a `CompletableFuture` that you can use to get the result, instead of using a `ConfirmCallback` on the template. +The following example shows how to configure a `CorrelationData` instance: + +[source, java] +---- +CorrelationData cd1 = new CorrelationData(); +this.templateWithConfirmsEnabled.convertAndSend("exchange", queue.getName(), "foo", cd1); +assertTrue(cd1.getFuture().get(10, TimeUnit.SECONDS).isAck()); +ReturnedMessage = cd1.getReturn(); +... +---- + +Since it is a `CompletableFuture`, you can either `get()` the result when ready or use `whenComplete()` for an asynchronous callback. +The `Confirm` object is a simple bean with 2 properties: `ack` and `reason` (for `nack` instances). +The reason is not populated for broker-generated `nack` instances. +It is populated for `nack` instances generated by the framework (for example, closing the connection while `ack` instances are outstanding). + +In addition, when both confirms and returns are enabled, the `CorrelationData` `return` property is populated with the returned message, if it couldn't be routed to any queue. +It is guaranteed that the returned message property is set before the future is set with the `ack`. +`CorrelationData.getReturn()` returns a `ReturnMessage` with properties: + +* message (the returned message) +* replyCode +* replyText +* exchange +* routingKey + +See also xref:amqp/template.adoc#scoped-operations[Scoped Operations] for a simpler mechanism for waiting for publisher confirms. + +[[scoped-operations]] +== Scoped Operations + +Normally, when using the template, a `Channel` is checked out of the cache (or created), used for the operation, and returned to the cache for reuse. +In a multi-threaded environment, there is no guarantee that the next operation uses the same channel. +There may be times, however, where you want to have more control over the use of a channel and ensure that a number of operations are all performed on the same channel. + +Starting with version 2.0, a new method called `invoke` is provided, with an `OperationsCallback`. +Any operations performed within the scope of the callback and on the provided `RabbitOperations` argument use the same dedicated `Channel`, which will be closed at the end (not returned to a cache). +If the channel is a `PublisherCallbackChannel`, it is returned to the cache after all confirms have been received (see xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns]). + +[source, java] +---- +@FunctionalInterface +public interface OperationsCallback { + + T doInRabbit(RabbitOperations operations); + +} +---- + +One example of why you might need this is if you wish to use the `waitForConfirms()` method on the underlying `Channel`. +This method was not previously exposed by the Spring API because the channel is, generally, cached and shared, as discussed earlier. +The `RabbitTemplate` now provides `waitForConfirms(long timeout)` and `waitForConfirmsOrDie(long timeout)`, which delegate to the dedicated channel used within the scope of the `OperationsCallback`. +The methods cannot be used outside of that scope, for obvious reasons. + +Note that a higher-level abstraction that lets you correlate confirms to requests is provided elsewhere (see xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns]). +If you want only to wait until the broker has confirmed delivery, you can use the technique shown in the following example: + +[source, java] +---- +Collection messages = getMessagesToSend(); +Boolean result = this.template.invoke(t -> { + messages.forEach(m -> t.convertAndSend(ROUTE, m)); + t.waitForConfirmsOrDie(10_000); + return true; +}); +---- + +If you wish `RabbitAdmin` operations to be invoked on the same channel within the scope of the `OperationsCallback`, the admin must have been constructed by using the same `RabbitTemplate` that was used for the `invoke` operation. + +NOTE: The preceding discussion is moot if the template operations are already performed within the scope of an existing transaction -- for example, when running on a transacted listener container thread and performing operations on a transacted template. +In that case, the operations are performed on that channel and committed when the thread returns to the container. +It is not necessary to use `invoke` in that scenario. + +When using confirms in this way, much of the infrastructure set up for correlating confirms to requests is not really needed (unless returns are also enabled). +Starting with version 2.2, the connection factory supports a new property called `publisherConfirmType`. +When this is set to `ConfirmType.SIMPLE`, the infrastructure is avoided and the confirm processing can be more efficient. + +Furthermore, the `RabbitTemplate` sets the `publisherSequenceNumber` property in the sent message `MessageProperties`. +If you wish to check (or log or otherwise use) specific confirms, you can do so with an overloaded `invoke` method, as the following example shows: + +[source, java] +---- +public T invoke(OperationsCallback action, com.rabbitmq.client.ConfirmCallback acks, + com.rabbitmq.client.ConfirmCallback nacks); +---- + +NOTE: These `ConfirmCallback` objects (for `ack` and `nack` instances) are the Rabbit client callbacks, not the template callback. + +The following example logs `ack` and `nack` instances: + +[source, java] +---- +Collection messages = getMessagesToSend(); +Boolean result = this.template.invoke(t -> { + messages.forEach(m -> t.convertAndSend(ROUTE, m)); + t.waitForConfirmsOrDie(10_000); + return true; +}, (tag, multiple) -> { + log.info("Ack: " + tag + ":" + multiple); +}, (tag, multiple) -> { + log.info("Nack: " + tag + ":" + multiple); +})); +---- + +IMPORTANT: Scoped operations are bound to a thread. +See xref:amqp/template.adoc#multi-strict[Strict Message Ordering in a Multi-Threaded Environment] for a discussion about strict ordering in a multi-threaded environment. + +[[multi-strict]] +== Strict Message Ordering in a Multi-Threaded Environment + +The discussion in xref:amqp/template.adoc#scoped-operations[Scoped Operations] applies only when the operations are performed on the same thread. + +Consider the following situation: + +* `thread-1` sends a message to a queue and hands off work to `thread-2` +* `thread-2` sends a message to the same queue + +Because of the async nature of RabbitMQ and the use of cached channels; it is not certain that the same channel will be used and therefore the order in which the messages arrive in the queue is not guaranteed. +(In most cases they will arrive in order, but the probability of out-of-order delivery is not zero). +To solve this use case, you can use a bounded channel cache with size `1` (together with a `channelCheckoutTimeout`) to ensure the messages are always published on the same channel, and order will be guaranteed. +To do this, if you have other uses for the connection factory, such as consumers, you should either use a dedicated connection factory for the template, or configure the template to use the publisher connection factory embedded in the main connection factory (see xref:amqp/template.adoc#separate-connection[Using a Separate Connection]). + +This is best illustrated with a simple Spring Boot Application: + +[source, java] +---- +@SpringBootApplication +public class Application { + + private static final Logger log = LoggerFactory.getLogger(Application.class); + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + TaskExecutor exec() { + ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor(); + exec.setCorePoolSize(10); + return exec; + } + + @Bean + CachingConnectionFactory ccf() { + CachingConnectionFactory ccf = new CachingConnectionFactory("localhost"); + CachingConnectionFactory publisherCF = (CachingConnectionFactory) ccf.getPublisherConnectionFactory(); + publisherCF.setChannelCacheSize(1); + publisherCF.setChannelCheckoutTimeout(1000L); + return ccf; + } + + @RabbitListener(queues = "queue") + void listen(String in) { + log.info(in); + } + + @Bean + Queue queue() { + return new Queue("queue"); + } + + + @Bean + public ApplicationRunner runner(Service service, TaskExecutor exec) { + return args -> { + exec.execute(() -> service.mainService("test")); + }; + } + +} + +@Component +class Service { + + private static final Logger LOG = LoggerFactory.getLogger(Service.class); + + private final RabbitTemplate template; + + private final TaskExecutor exec; + + Service(RabbitTemplate template, TaskExecutor exec) { + template.setUsePublisherConnection(true); + this.template = template; + this.exec = exec; + } + + void mainService(String toSend) { + LOG.info("Publishing from main service"); + this.template.convertAndSend("queue", toSend); + this.exec.execute(() -> secondaryService(toSend.toUpperCase())); + } + + void secondaryService(String toSend) { + LOG.info("Publishing from secondary service"); + this.template.convertAndSend("queue", toSend); + } + +} +---- + +Even though the publishing is performed on two different threads, they will both use the same channel because the cache is capped at a single channel. + +Starting with version 2.3.7, the `ThreadChannelConnectionFactory` supports transferring a thread's channel(s) to another thread, using the `prepareContextSwitch` and `switchContext` methods. +The first method returns a context which is passed to the second thread which calls the second method. +A thread can have either a non-transactional channel or a transactional channel (or one of each) bound to it; you cannot transfer them individually, unless you use two connection factories. +An example follows: + +[source, java] +---- +@SpringBootApplication +public class Application { + + private static final Logger log = LoggerFactory.getLogger(Application.class); + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + TaskExecutor exec() { + ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor(); + exec.setCorePoolSize(10); + return exec; + } + + @Bean + ThreadChannelConnectionFactory tccf() { + ConnectionFactory rabbitConnectionFactory = new ConnectionFactory(); + rabbitConnectionFactory.setHost("localhost"); + return new ThreadChannelConnectionFactory(rabbitConnectionFactory); + } + + @RabbitListener(queues = "queue") + void listen(String in) { + log.info(in); + } + + @Bean + Queue queue() { + return new Queue("queue"); + } + + + @Bean + public ApplicationRunner runner(Service service, TaskExecutor exec) { + return args -> { + exec.execute(() -> service.mainService("test")); + }; + } + +} + +@Component +class Service { + + private static final Logger LOG = LoggerFactory.getLogger(Service.class); + + private final RabbitTemplate template; + + private final TaskExecutor exec; + + private final ThreadChannelConnectionFactory connFactory; + + Service(RabbitTemplate template, TaskExecutor exec, + ThreadChannelConnectionFactory tccf) { + + this.template = template; + this.exec = exec; + this.connFactory = tccf; + } + + void mainService(String toSend) { + LOG.info("Publishing from main service"); + this.template.convertAndSend("queue", toSend); + Object context = this.connFactory.prepareSwitchContext(); + this.exec.execute(() -> secondaryService(toSend.toUpperCase(), context)); + } + + void secondaryService(String toSend, Object threadContext) { + LOG.info("Publishing from secondary service"); + this.connFactory.switchContext(threadContext); + this.template.convertAndSend("queue", toSend); + this.connFactory.closeThreadChannel(); + } + +} +---- + +IMPORTANT: Once the `prepareSwitchContext` is called, if the current thread performs any more operations, they will be performed on a new channel. +It is important to close the thread-bound channel when it is no longer needed. + +[[template-messaging]] +== Messaging Integration + +Starting with version 1.4, `RabbitMessagingTemplate` (built on top of `RabbitTemplate`) provides an integration with the Spring Framework messaging abstraction -- that is, +`org.springframework.messaging.Message`. +This lets you send and receive messages by using the `spring-messaging` `Message` abstraction. +This abstraction is used by other Spring projects, such as Spring Integration and Spring's STOMP support. +There are two message converters involved: one to convert between a spring-messaging `Message` and Spring AMQP's `Message` abstraction and one to convert between Spring AMQP's `Message` abstraction and the format required by the underlying RabbitMQ client library. +By default, the message payload is converted by the provided `RabbitTemplate` instance's message converter. +Alternatively, you can inject a custom `MessagingMessageConverter` with some other payload converter, as the following example shows: + +[source, java] +---- +MessagingMessageConverter amqpMessageConverter = new MessagingMessageConverter(); +amqpMessageConverter.setPayloadConverter(myPayloadConverter); +rabbitMessagingTemplate.setAmqpMessageConverter(amqpMessageConverter); +---- + +[[template-user-id]] +== Validated User Id + +Starting with version 1.6, the template now supports a `user-id-expression` (`userIdExpression` when using Java configuration). +If a message is sent, the user id property is set (if not already set) after evaluating this expression. +The root object for the evaluation is the message to be sent. + +The following examples show how to use the `user-id-expression` attribute: + +[source, xml] +---- + + + +---- + +The first example is a literal expression. +The second obtains the `username` property from a connection factory bean in the application context. + +[[separate-connection]] +== Using a Separate Connection + +Starting with version 2.0.2, you can set the `usePublisherConnection` property to `true` to use a different connection to that used by listener containers, when possible. +This is to avoid consumers being blocked when a producer is blocked for any reason. +The connection factories maintain a second internal connection factory for this purpose; by default it is the same type as the main factory, but can be set explicitly if you wish to use a different factory type for publishing. +If the rabbit template is running in a transaction started by the listener container, the container's channel is used, regardless of this setting. + +IMPORTANT: In general, you should not use a `RabbitAdmin` with a template that has this set to `true`. +Use the `RabbitAdmin` constructor that takes a connection factory. +If you use the other constructor that takes a template, ensure the template's property is `false`. +This is because, often, an admin is used to declare queues for listener containers. +Using a template that has the property set to `true` would mean that exclusive queues (such as `AnonymousQueue`) would be declared on a different connection to that used by listener containers. +In that case, the queues cannot be used by the containers. + diff --git a/src/reference/antora/modules/ROOT/pages/amqp/transactions.adoc b/src/reference/antora/modules/ROOT/pages/amqp/transactions.adoc new file mode 100644 index 0000000000..fad761b9f0 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/amqp/transactions.adoc @@ -0,0 +1,157 @@ +[[transactions]] += Transactions + +The Spring Rabbit framework has support for automatic transaction management in the synchronous and asynchronous use cases with a number of different semantics that can be selected declaratively, as is familiar to existing users of Spring transactions. +This makes many if not most common messaging patterns easy to implement. + +There are two ways to signal the desired transaction semantics to the framework. +In both the `RabbitTemplate` and `SimpleMessageListenerContainer`, there is a flag `channelTransacted` which, if `true`, tells the framework to use a transactional channel and to end all operations (send or receive) with a commit or rollback (depending on the outcome), with an exception signaling a rollback. +Another signal is to provide an external transaction with one of Spring's `PlatformTransactionManager` implementations as a context for the ongoing operation. +If there is already a transaction in progress when the framework is sending or receiving a message, and the `channelTransacted` flag is `true`, the commit or rollback of the messaging transaction is deferred until the end of the current transaction. +If the `channelTransacted` flag is `false`, no transaction semantics apply to the messaging operation (it is auto-acked). + +The `channelTransacted` flag is a configuration time setting. +It is declared and processed once when the AMQP components are created, usually at application startup. +The external transaction is more dynamic in principle because the system responds to the current thread state at runtime. +However, in practice, it is often also a configuration setting, when the transactions are layered onto an application declaratively. + +For synchronous use cases with `RabbitTemplate`, the external transaction is provided by the caller, either declaratively or imperatively according to taste (the usual Spring transaction model). +The following example shows a declarative approach (usually preferred because it is non-invasive), where the template has been configured with `channelTransacted=true`: + +[source,java] +---- +@Transactional +public void doSomething() { + String incoming = rabbitTemplate.receiveAndConvert(); + // do some more database processing... + String outgoing = processInDatabaseAndExtractReply(incoming); + rabbitTemplate.convertAndSend(outgoing); +} +---- + +In the preceding example, a `String` payload is received, converted, and sent as a message body inside a method marked as `@Transactional`. +If the database processing fails with an exception, the incoming message is returned to the broker, and the outgoing message is not sent. +This applies to any operations with the `RabbitTemplate` inside a chain of transactional methods (unless, for instance, the `Channel` is directly manipulated to commit the transaction early). + +For asynchronous use cases with `SimpleMessageListenerContainer`, if an external transaction is needed, it has to be requested by the container when it sets up the listener. +To signal that an external transaction is required, the user provides an implementation of `PlatformTransactionManager` to the container when it is configured. +The following example shows how to do so: + +[source,java] +---- +@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; + } + +} +---- + +In the preceding example, the transaction manager is added as a dependency injected from another bean definition (not shown), and the `channelTransacted` flag is also set to `true`. +The effect is that if the listener fails with an exception, the transaction is rolled back, and the message is also returned to the broker. +Significantly, if the transaction fails to commit (for example, because of +a database constraint error or connectivity problem), the AMQP transaction is also rolled back, and the message is returned to the broker. +This is sometimes known as a "`Best Efforts 1 Phase Commit`", and is a very powerful pattern for reliable messaging. +If the `channelTransacted` flag was set to `false` (the default) in the preceding example, the external transaction would still be provided for the listener, but all messaging operations would be auto-acked, so the effect is to commit the messaging operations even on a rollback of the business operation. + +[[conditional-rollback]] +== Conditional Rollback + +Prior to version 1.6.6, adding a rollback rule to a container's `transactionAttribute` when using an external transaction manager (such as JDBC) had no effect. +Exceptions always rolled back the transaction. + +Also, when using a https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/html/transaction.html#transaction-declarative[transaction advice] in the container's advice chain, conditional rollback was not very useful, because all listener exceptions are wrapped in a `ListenerExecutionFailedException`. + +The first problem has been corrected, and the rules are now applied properly. +Further, the `ListenerFailedRuleBasedTransactionAttribute` is now provided. +It is a subclass of `RuleBasedTransactionAttribute`, with the only difference being that it is aware of the `ListenerExecutionFailedException` and uses the cause of such exceptions for the rule. +This transaction attribute can be used directly in the container or through a transaction advice. + +The following example uses this rule: + +[source, java] +---- +@Bean +public AbstractMessageListenerContainer container() { + ... + container.setTransactionManager(transactionManager); + RuleBasedTransactionAttribute transactionAttribute = + new ListenerFailedRuleBasedTransactionAttribute(); + transactionAttribute.setRollbackRules(Collections.singletonList( + new NoRollbackRuleAttribute(DontRollBackException.class))); + container.setTransactionAttribute(transactionAttribute); + ... +} +---- + +[[transaction-rollback]] +== A note on Rollback of Received Messages + +AMQP transactions apply only to messages and acks sent to the broker. +Consequently, when there is a rollback of a Spring transaction and a message has been received, Spring AMQP has to not only rollback the transaction but also manually reject the message (sort of a nack, but that is not what the specification calls it). +The action taken on message rejection is independent of transactions and depends on the `defaultRequeueRejected` property (default: `true`). +For more information about rejecting failed messages, see xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc#async-listeners[Message Listeners and the Asynchronous Case]. + +For more information about RabbitMQ transactions and their limitations, see https://www.rabbitmq.com/semantics.html[RabbitMQ Broker Semantics]. + +NOTE: Prior to RabbitMQ 2.7.0, such messages (and any that are unacked when a channel is closed or aborts) went to the back of the queue on a Rabbit broker. +Since 2.7.0, rejected messages go to the front of the queue, in a similar manner to JMS rolled back messages. + +NOTE: Previously, message requeue on transaction rollback was inconsistent between local transactions and when a `TransactionManager` was provided. +In the former case, the normal requeue logic (`AmqpRejectAndDontRequeueException` or `defaultRequeueRejected=false`) applied (see xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc#async-listeners[Message Listeners and the Asynchronous Case]). +With a transaction manager, the message was unconditionally requeued on rollback. +Starting with version 2.0, the behavior is consistent and the normal requeue logic is applied in both cases. +To revert to the previous behavior, you can set the container's `alwaysRequeueWithTxManagerRollback` property to `true`. +See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration]. + +[[using-rabbittransactionmanager]] +== Using `RabbitTransactionManager` + +The https://docs.spring.io/spring-amqp/docs/latest_ga/api/org/springframework/amqp/rabbit/transaction/RabbitTransactionManager.html[RabbitTransactionManager] is an alternative to executing Rabbit operations within, and synchronized with, external transactions. +This transaction manager is an implementation of the https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/transaction/PlatformTransactionManager.html[`PlatformTransactionManager`] interface and should be used with a single Rabbit `ConnectionFactory`. + +IMPORTANT: This strategy is not able to provide XA transactions -- for example, in order to share transactions between messaging and database access. + +Application code is required to retrieve the transactional Rabbit resources through `ConnectionFactoryUtils.getTransactionalResourceHolder(ConnectionFactory, boolean)` instead of a standard `Connection.createChannel()` call with subsequent channel creation. +When using Spring AMQP's https://docs.spring.io/spring-amqp/docs/latest_ga/api/org/springframework/amqp/rabbit/core/RabbitTemplate.html[RabbitTemplate], it will autodetect a thread-bound Channel and automatically participate in its transaction. + +With Java Configuration, you can setup a new RabbitTransactionManager by using the following bean: + +[source,java] +---- +@Bean +public RabbitTransactionManager rabbitTransactionManager() { + return new RabbitTransactionManager(connectionFactory); +} +---- + +If you prefer XML configuration, you can declare the following bean in your XML Application Context file: + +[source,xml] +---- + + + +---- + +[[tx-sync]] +== Transaction Synchronization + +Synchronizing a RabbitMQ transaction with some other (e.g. DBMS) transaction provides "Best Effort One Phase Commit" semantics. +It is possible that the RabbitMQ transaction fails to commit during the after completion phase of transaction synchronization. +This is logged by the `spring-tx` infrastructure as an error, but no exception is thrown to the calling code. +Starting with version 2.3.10, you can call `ConnectionUtils.checkAfterCompletion()` after the transaction has committed on the same thread that processed the transaction. +It will simply return if no exception occurred; otherwise it will throw an `AfterCompletionFailedException` which will have a property representing the synchronization status of the completion. + +Enable this feature by calling `ConnectionFactoryUtils.enableAfterCompletionFailureCapture(true)`; this is a global flag and applies to all threads. + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/change-history.adoc b/src/reference/antora/modules/ROOT/pages/appendix/change-history.adoc new file mode 100644 index 0000000000..85b97003ba --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/change-history.adoc @@ -0,0 +1,5 @@ +[[change-history]] += Change History +:page-section-summary-toc: 1 + +This section describes changes that have been made as versions have changed. diff --git a/src/reference/antora/modules/ROOT/pages/appendix/current-release.adoc b/src/reference/antora/modules/ROOT/pages/appendix/current-release.adoc new file mode 100644 index 0000000000..e76e79e541 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/current-release.adoc @@ -0,0 +1,6 @@ +[[current-release]] += Current Release +:page-section-summary-toc: 1 + +See xref:whats-new.adoc[What's New]. + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/micrometer.adoc b/src/reference/antora/modules/ROOT/pages/appendix/micrometer.adoc new file mode 100644 index 0000000000..0ec7789c76 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/micrometer.adoc @@ -0,0 +1,8 @@ +[[observation-gen]] += Micrometer Observation Documentation + +This section describes the Micrometer integration. + +include::partial$metrics.adoc[leveloffset=-1] +include::partial$spans.adoc[leveloffset=-1] +include::partial$conventions.adoc[leveloffset=-1] diff --git a/src/reference/antora/modules/ROOT/pages/appendix/native.adoc b/src/reference/antora/modules/ROOT/pages/appendix/native.adoc new file mode 100644 index 0000000000..ee924671a2 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/native.adoc @@ -0,0 +1,7 @@ +[[native-images]] += Native Images +:page-section-summary-toc: 1 + +https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aot[Spring AOT] native hints are provided to assist in developing native images for Spring applications that use Spring AMQP. + +Some examples can be seen in the https://github.com/spring-projects/spring-aot-smoke-tests/tree/main/integration[`spring-aot-smoke-tests` GitHub repository]. diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new.adoc new file mode 100644 index 0000000000..d9f5c27873 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new.adoc @@ -0,0 +1,4 @@ +[[previous-whats-new]] += Previous Releases +:page-section-summary-toc: 1 + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-3-since-1-2.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-3-since-1-2.adoc new file mode 100644 index 0000000000..da13067fff --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-3-since-1-2.adoc @@ -0,0 +1,107 @@ +[[changes-in-1-3-since-1-2]] += Changes in 1.3 Since 1.2 + +[[listener-concurrency]] +== Listener Concurrency + +The listener container now supports dynamic scaling of the number of consumers based on workload, or you can programmatically change the concurrency without stopping the container. +See <>. + +[[listener-queues]] +== Listener Queues + +The listener container now permits the queues on which it listens to be modified at runtime. +Also, the container now starts if at least one of its configured queues is available for use. +See <> + +This listener container now redeclares any auto-delete queues during startup. +See xref:amqp/receiving-messages/async-consumer.adoc#lc-auto-delete[`auto-delete` Queues]. + +[[consumer-priority]] +== Consumer Priority + +The listener container now supports consumer arguments, letting the `x-priority` argument be set. +See <>. + +[[exclusive-consumer]] +== Exclusive Consumer + +You can now configure `SimpleMessageListenerContainer` with a single `exclusive` consumer, preventing other consumers from listening to the queue. +See <>. + +[[rabbit-admin]] +== Rabbit Admin + +You can now have the broker generate the queue name, regardless of `durable`, `autoDelete`, and `exclusive` settings. +See xref:amqp/broker-configuration.adoc[Configuring the Broker]. + +[[direct-exchange-binding]] +== Direct Exchange Binding + +Previously, omitting the `key` attribute from a `binding` element of a `direct-exchange` configuration caused the queue or exchange to be bound with an empty string as the routing key. +Now it is bound with the the name of the provided `Queue` or `Exchange`. +If you wish to bind with an empty string routing key, you need to specify `key=""`. + +[[amqptemplate-changes]] +== `AmqpTemplate` Changes + +The `AmqpTemplate` now provides several synchronous `receiveAndReply` methods. +These are implemented by the `RabbitTemplate`. +For more information see <>. + +The `RabbitTemplate` now supports configuring a `RetryTemplate` to attempt retries (with optional back-off policy) for when the broker is not available. +For more information see xref:amqp/template.adoc#template-retry[Adding Retry Capabilities]. + +[[caching-connection-factory]] +== Caching Connection Factory + +You can now configure the caching connection factory to cache `Connection` instances and their `Channel` instances instead of using a single connection and caching only `Channel` instances. +See xref:amqp/connections.adoc[Connection and Resource Management]. + +[[binding-arguments]] +== Binding Arguments + +The `` of the `` now supports parsing of the `` sub-element. +You can now configure the `` of the `` with a `key/value` attribute pair (to match on a single header) or with a `` sub-element (allowing matching on multiple headers). +These options are mutually exclusive. +See xref:amqp/broker-configuration.adoc#headers-exchange[Headers Exchange]. + +[[routing-connection-factory]] +== Routing Connection Factory + +A new `SimpleRoutingConnectionFactory` has been introduced. +It allows configuration of `ConnectionFactories` mapping, to determine the target `ConnectionFactory` to use at runtime. +See <>. + +[[messagebuilder-and-messagepropertiesbuilder]] +== `MessageBuilder` and `MessagePropertiesBuilder` + +"`Fluent APIs`" for building messages or message properties are now provided. +See xref:amqp/sending-messages.adoc#message-builder[Message Builder API]. + +[[retryinterceptorbuilder-change]] +== `RetryInterceptorBuilder` Change + +A "`Fluent API`" for building listener container retry interceptors is now provided. +See xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc#retry[Failures in Synchronous Operations and Options for Retry]. + +[[republishmessagerecoverer-added]] +== `RepublishMessageRecoverer` Added + +This new `MessageRecoverer` is provided to allow publishing a failed message to another queue (including stack trace information in the header) when retries are exhausted. +See xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc#async-listeners[Message Listeners and the Asynchronous Case]. + +[[default-error-handler-since-1-3-2]] +== Default Error Handler (Since 1.3.2) + +A default `ConditionalRejectingErrorHandler` has been added to the listener container. +This error handler detects fatal message conversion problems and instructs the container to reject the message to prevent the broker from continually redelivering the unconvertible message. +See xref:amqp/exception-handling.adoc[Exception Handling]. + +[[listener-container-missingqueuesfatal-property-since-1-3-5]] +== Listener Container 'missingQueuesFatal` Property (Since 1.3.5) + +The `SimpleMessageListenerContainer` now has a property called `missingQueuesFatal` (default: `true`). +Previously, missing queues were always fatal. +See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration]. + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-4-since-1-3.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-4-since-1-3.adoc new file mode 100644 index 0000000000..a4346e4ba4 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-4-since-1-3.adoc @@ -0,0 +1,120 @@ +[[changes-in-1-4-since-1-3]] += Changes in 1.4 Since 1.3 + +[[rabbitlistener-annotation]] +== `@RabbitListener` Annotation + +POJO listeners can be annotated with `@RabbitListener`, enabled by `@EnableRabbit` or ``. +Spring Framework 4.1 is required for this feature. +See xref:amqp/receiving-messages/async-annotation-driven.adoc[Annotation-driven Listener Endpoints] for more information. + +[[rabbitmessagingtemplate-added]] +== `RabbitMessagingTemplate` Added + +A new `RabbitMessagingTemplate` lets you interact with RabbitMQ by using `spring-messaging` `Message` instances. +Internally, it uses the `RabbitTemplate`, which you can configure as normal. +Spring Framework 4.1 is required for this feature. +See xref:amqp/template.adoc#template-messaging[Messaging Integration] for more information. + +[[listener-container-missingqueuesfatal-attribute]] +== Listener Container `missingQueuesFatal` Attribute + +1.3.5 introduced the `missingQueuesFatal` property on the `SimpleMessageListenerContainer`. +This is now available on the listener container namespace element. +See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration]. + +[[rabbittemplate-confirmcallback-interface]] +== RabbitTemplate `ConfirmCallback` Interface + +The `confirm` method on this interface has an additional parameter called `cause`. +When available, this parameter contains the reason for a negative acknowledgement (nack). +See xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns]. + +[[rabbitconnectionfactorybean-added]] +== `RabbitConnectionFactoryBean` Added + +`RabbitConnectionFactoryBean` creates the underlying RabbitMQ `ConnectionFactory` used by the `CachingConnectionFactory`. +This enables configuration of SSL options using Spring's dependency injection. +See <>. + +[[using-cachingconnectionfactory]] +== Using `CachingConnectionFactory` + +The `CachingConnectionFactory` now lets the `connectionTimeout` be set as a property or as an attribute in the namespace. +It sets the property on the underlying RabbitMQ `ConnectionFactory`. +See <>. + +[[log-appender]] +== Log Appender + +The Logback `org.springframework.amqp.rabbit.logback.AmqpAppender` has been introduced. +It provides options similar to `org.springframework.amqp.rabbit.log4j.AmqpAppender`. +For more information, see the JavaDoc of these classes. + +The Log4j `AmqpAppender` now supports the `deliveryMode` property (`PERSISTENT` or `NON_PERSISTENT`, default: `PERSISTENT`). +Previously, all log4j messages were `PERSISTENT`. + +The appender also supports modification of the `Message` before sending -- allowing, for example, the addition of custom headers. +Subclasses should override the `postProcessMessageBeforeSend()`. + +[[listener-queues]] +== Listener Queues + +The listener container now, by default, redeclares any missing queues during startup. +A new `auto-declare` attribute has been added to the `` to prevent these re-declarations. +See xref:amqp/receiving-messages/async-consumer.adoc#lc-auto-delete[`auto-delete` Queues]. + +[[rabbittemplate:-mandatory-and-connectionfactoryselector-expressions]] +== `RabbitTemplate`: `mandatory` and `connectionFactorySelector` Expressions + +The `mandatoryExpression`, `sendConnectionFactorySelectorExpression`, and `receiveConnectionFactorySelectorExpression` SpEL Expression`s properties have been added to `RabbitTemplate`. +The `mandatoryExpression` is used to evaluate a `mandatory` boolean value against each request message when a `ReturnCallback` is in use. +See xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns]. +The `sendConnectionFactorySelectorExpression` and `receiveConnectionFactorySelectorExpression` are used when an `AbstractRoutingConnectionFactory` is provided, to determine the `lookupKey` for the target `ConnectionFactory` at runtime on each AMQP protocol interaction operation. +See <>. + +[[listeners-and-the-routing-connection-factory]] +== Listeners and the Routing Connection Factory + +You can configure a `SimpleMessageListenerContainer` with a routing connection factory to enable connection selection based on the queue names. +See <>. + +[[rabbittemplate:-recoverycallback-option]] +== `RabbitTemplate`: `RecoveryCallback` Option + +The `recoveryCallback` property has been added for use in the `retryTemplate.execute()`. +See xref:amqp/template.adoc#template-retry[Adding Retry Capabilities]. + +[[messageconversionexception-change]] +== `MessageConversionException` Change + +This exception is now a subclass of `AmqpException`. +Consider the following code: + +[source,java] +---- +try { + template.convertAndSend("thing1", "thing2", "cat"); +} +catch (AmqpException e) { + ... +} +catch (MessageConversionException e) { + ... +} +---- + +The second catch block is no longer reachable and needs to be moved above the catch-all `AmqpException` catch block. + +[[rabbitmq-3-4-compatibility]] +== RabbitMQ 3.4 Compatibility + +Spring AMQP is now compatible with the RabbitMQ 3.4, including direct reply-to. +See xref:introduction/quick-tour.adoc#compatibility[Compatibility] and xref:amqp/request-reply.adoc#direct-reply-to[RabbitMQ Direct reply-to] for more information. + +[[contenttypedelegatingmessageconverter-added]] +== `ContentTypeDelegatingMessageConverter` Added + +The `ContentTypeDelegatingMessageConverter` has been introduced to select the `MessageConverter` to use, based on the `contentType` property in the `MessageProperties`. +See xref:amqp/message-converters.adoc[Message Converters] for more information. + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-5-since-1-4.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-5-since-1-4.adoc new file mode 100644 index 0000000000..29282a0033 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-5-since-1-4.adoc @@ -0,0 +1,197 @@ +[[changes-in-1-5-since-1-4]] += Changes in 1.5 Since 1.4 + +[[spring-erlang-is-no-longer-supported]] +== `spring-erlang` Is No Longer Supported + +The `spring-erlang` jar is no longer included in the distribution. +Use <> instead. + +[[cachingconnectionfactory-changes]] +== `CachingConnectionFactory` Changes + +[[empty-addresses-property-in-cachingconnectionfactory]] +=== Empty Addresses Property in `CachingConnectionFactory` + +Previously, if the connection factory was configured with a host and port but an empty String was also supplied for +`addresses`, the host and port were ignored. +Now, an empty `addresses` String is treated the same as a `null`, and the host and port are used. + +[[uri-constructor]] +=== URI Constructor + +The `CachingConnectionFactory` has an additional constructor, with a `URI` parameter, to configure the broker connection. + +[[connection-reset]] +=== Connection Reset + +A new method called `resetConnection()` has been added to let users reset the connection (or connections). +You might use this, for example, to reconnect to the primary broker after failing over to the secondary broker. +This *does* impact in-process operations. +The existing `destroy()` method does exactly the same, but the new method has a less daunting name. + +[[properties-to-control-container-queue-declaration-behavior]] +== Properties to Control Container Queue Declaration Behavior + +When the listener container consumers start, they attempt to passively declare the queues to ensure they are available +on the broker. +Previously, if these declarations failed (for example, because the queues didn't exist) or when an HA queue was being +moved, the retry logic was fixed at three retry attempts at five-second intervals. +If the queues still do not exist, the behavior is controlled by the `missingQueuesFatal` property (default: `true`). +Also, for containers configured to listen from multiple queues, if only a subset of queues are available, the consumer +retried the missing queues on a fixed interval of 60 seconds. + +The `declarationRetries`, `failedDeclarationRetryInterval`, and `retryDeclarationInterval` properties are now configurable. +See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration] for more information. + +[[class-package-change]] +== Class Package Change + +The `RabbitGatewaySupport` class has been moved from `o.s.amqp.rabbit.core.support` to `o.s.amqp.rabbit.core`. + +[[defaultmessagepropertiesconverter-changes]] +== `DefaultMessagePropertiesConverter` Changes + +You can now configure the `DefaultMessagePropertiesConverter` to +determine the maximum length of a `LongString` that is converted +to a `String` rather than to a `DataInputStream`. +The converter has an alternative constructor that takes the value as a limit. +Previously, this limit was hard-coded at `1024` bytes. +(Also available in 1.4.4). + +[[rabbitlistener-improvements]] +== `@RabbitListener` Improvements + +[[queuebinding-for-rabbitlistener]] +=== `@QueueBinding` for `@RabbitListener` + +The `bindings` attribute has been added to the `@RabbitListener` annotation as mutually exclusive with the `queues` +attribute to allow the specification of the `queue`, its `exchange`, and `binding` for declaration by a `RabbitAdmin` on +the Broker. + +[[spel-in-sendto]] +=== SpEL in `@SendTo` + +The default reply address (`@SendTo`) for a `@RabbitListener` can now be a SpEL expression. + +[[multiple-queue-names-through-properties]] +=== Multiple Queue Names through Properties + +You can now use a combination of SpEL and property placeholders to specify multiple queues for a listener. + +See xref:amqp/receiving-messages/async-annotation-driven.adoc[Annotation-driven Listener Endpoints] for more information. + +[[automatic-exchange-queue-and-binding-declaration]] +== Automatic Exchange, Queue, and Binding Declaration + +You can now declare beans that define a collection of these entities, and the `RabbitAdmin` adds the +contents to the list of entities that it declares when a connection is established. +See xref:amqp/broker-configuration.adoc#collection-declaration[Declaring Collections of Exchanges, Queues, and Bindings] for more information. + +[[rabbittemplate-changes]] +== `RabbitTemplate` Changes + +[[reply-address-added]] +=== `reply-address` Added + +The `reply-address` attribute has been added to the `` component as an alternative `reply-queue`. +See xref:amqp/request-reply.adoc[Request/Reply Messaging] for more information. +(Also available in 1.4.4 as a setter on the `RabbitTemplate`). + +[[blocking-receive-methods]] +=== Blocking `receive` Methods + +The `RabbitTemplate` now supports blocking in `receive` and `convertAndReceive` methods. +See xref:amqp/receiving-messages/polling-consumer.adoc[Polling Consumer] for more information. + +[[mandatory-with-sendandreceive-methods]] +=== Mandatory with `sendAndReceive` Methods + +When the `mandatory` flag is set when using the `sendAndReceive` and `convertSendAndReceive` methods, the calling thread +throws an `AmqpMessageReturnedException` if the request message cannot be delivered. +See xref:amqp/request-reply.adoc#reply-timeout[Reply Timeout] for more information. + +[[improper-reply-listener-configuration]] +=== Improper Reply Listener Configuration + +The framework tries to verify proper configuration of a reply listener container when using a named reply queue. + +See xref:amqp/request-reply.adoc#reply-listener[Reply Listener Container] for more information. + +[[rabbitmanagementtemplate-added]] +== `RabbitManagementTemplate` Added + +The `RabbitManagementTemplate` has been introduced to monitor and configure the RabbitMQ Broker by using the REST API provided by its https://www.rabbitmq.com/management.html[management plugin]. +See <> for more information. + +[[listener-container-bean-names-xml]] +== Listener Container Bean Names (XML) + +[IMPORTANT] +==== +The `id` attribute on the `` element has been removed. +Starting with this release, the `id` on the `` child element is used alone to name the listener container bean created for each listener element. + +Normal Spring bean name overrides are applied. +If a later `` is parsed with the same `id` as an existing bean, the new definition overrides the existing one. +Previously, bean names were composed from the `id` attributes of the `` and `` elements. + +When migrating to this release, if you have `id` attributes on your `` elements, remove them and set the `id` on the child `` element instead. +==== + +However, to support starting and stopping containers as a group, a new `group` attribute has been added. +When this attribute is defined, the containers created by this element are added to a bean with this name, of type `Collection`. +You can iterate over this group to start and stop containers. + +[[class-level-rabbitlistener]] +== Class-Level `@RabbitListener` + +The `@RabbitListener` annotation can now be applied at the class level. +Together with the new `@RabbitHandler` method annotation, this lets you select the handler method based on payload type. +See xref:amqp/receiving-messages/async-annotation-driven/method-selection.adoc[Multi-method Listeners] for more information. + +[[simplemessagelistenercontainer:-backoff-support]] +== `SimpleMessageListenerContainer`: BackOff Support + +The `SimpleMessageListenerContainer` can now be supplied with a `BackOff` instance for `consumer` startup recovery. +See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration] for more information. + +[[channel-close-logging]] +== Channel Close Logging + +A mechanism to control the log levels of channel closure has been introduced. +See <>. + +[[application-events]] +== Application Events + +The `SimpleMessageListenerContainer` now emits application events when consumers fail. +See xref:amqp/receiving-messages/consumer-events.adoc[Consumer Events] for more information. + +[[consumer-tag-configuration]] +== Consumer Tag Configuration + +Previously, the consumer tags for asynchronous consumers were generated by the broker. +With this release, it is now possible to supply a naming strategy to the listener container. +See xref:amqp/receiving-messages/consumerTags.adoc[Consumer Tags]. + +[[using-messagelisteneradapter]] +== Using `MessageListenerAdapter` + +The `MessageListenerAdapter` now supports a map of queue names (or consumer tags) to method names, to determine +which delegate method to call based on the queue from which the message was received. + +[[localizedqueueconnectionfactory-added]] +== `LocalizedQueueConnectionFactory` Added + +`LocalizedQueueConnectionFactory` is a new connection factory that connects to the node in a cluster where a mirrored queue actually resides. + +See xref:amqp/connections.adoc#queue-affinity[Queue Affinity and the `LocalizedQueueConnectionFactory`]. + +[[anonymous-queue-naming]] +== Anonymous Queue Naming + +Starting with version 1.5.3, you can now control how `AnonymousQueue` names are generated. +See xref:amqp/broker-configuration.adoc#anonymous-queue[`AnonymousQueue`] for more information. + + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-6-since-1-5.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-6-since-1-5.adoc new file mode 100644 index 0000000000..237a3053bc --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-6-since-1-5.adoc @@ -0,0 +1,262 @@ +[[changes-in-1-6-since-1-5]] += Changes in 1.6 Since 1.5 + +[[testing-support]] +== Testing Support + +A new testing support library is now provided. +See xref:testing.adoc[Testing Support] for more information. + +[[builder]] +== Builder + +Builders that provide a fluent API for configuring `Queue` and `Exchange` objects are now available. +See xref:amqp/broker-configuration.adoc#builder-api[Builder API for Queues and Exchanges] for more information. + +[[namespace-changes]] +== Namespace Changes + +[[connection-factory]] +=== Connection Factory + +You can now add a `thread-factory` to a connection factory bean declaration -- for example, to name the threads +created by the `amqp-client` library. +See xref:amqp/connections.adoc[Connection and Resource Management] for more information. + +When you use `CacheMode.CONNECTION`, you can now limit the total number of connections allowed. +See xref:amqp/connections.adoc[Connection and Resource Management] for more information. + +[[queue-definitions]] +=== Queue Definitions + +You can now provide a naming strategy for anonymous queues. +See xref:amqp/broker-configuration.adoc#anonymous-queue[`AnonymousQueue`] for more information. + +[[listener-container-changes]] +== Listener Container Changes + +[[idle-message-listener-detection]] +=== Idle Message Listener Detection + +You can now configure listener containers to publish `ApplicationEvent` instances when idle. +See xref:amqp/receiving-messages/idle-containers.adoc[Detecting Idle Asynchronous Consumers] for more information. + +[[mismatched-queue-detection]] +=== Mismatched Queue Detection + +By default, when a listener container starts, if queues with mismatched properties or arguments are detected, +the container logs the exception but continues to listen. +The container now has a property called `mismatchedQueuesFatal`, which prevents the container (and context) from +starting if the problem is detected during startup. +It also stops the container if the problem is detected later, such as after recovering from a connection failure. +See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration] for more information. + +[[listener-container-logging]] +=== Listener Container Logging + +Now, listener container provides its `beanName` to the internal `SimpleAsyncTaskExecutor` as a `threadNamePrefix`. +It is useful for logs analysis. + +[[default-error-handler]] +=== Default Error Handler + +The default error handler (`ConditionalRejectingErrorHandler`) now considers irrecoverable `@RabbitListener` +exceptions as fatal. +See xref:amqp/exception-handling.adoc[Exception Handling] for more information. + + +[[autodeclare-and-rabbitadmin-instances]] +== `AutoDeclare` and `RabbitAdmin` Instances + +See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration] (`autoDeclare`) for some changes to the semantics of that option with respect to the use +of `RabbitAdmin` instances in the application context. + +[[amqptemplate:-receive-with-timeout]] +== `AmqpTemplate`: Receive with Timeout + +A number of new `receive()` methods with `timeout` have been introduced for the `AmqpTemplate` +and its `RabbitTemplate` implementation. +See xref:amqp/receiving-messages/polling-consumer.adoc[Polling Consumer] for more information. + +[[using-asyncrabbittemplate]] +== Using `AsyncRabbitTemplate` + +A new `AsyncRabbitTemplate` has been introduced. +This template provides a number of send and receive methods, where the return value is a `ListenableFuture`, which can +be used later to obtain the result either synchronously or asynchronously. +See xref:amqp/request-reply.adoc#async-template[Async Rabbit Template] for more information. + +[[rabbittemplate-changes]] +== `RabbitTemplate` Changes + +1.4.1 introduced the ability to use https://www.rabbitmq.com/direct-reply-to.html[direct reply-to] when the broker supports it. +It is more efficient than using a temporary queue for each reply. +This version lets you override this default behavior and use a temporary queue by setting the `useTemporaryReplyQueues` property to `true`. +See xref:amqp/request-reply.adoc#direct-reply-to[RabbitMQ Direct reply-to] for more information. + +The `RabbitTemplate` now supports a `user-id-expression` (`userIdExpression` when using Java configuration). +See https://www.rabbitmq.com/validated-user-id.html[Validated User-ID RabbitMQ documentation] and xref:amqp/template.adoc#template-user-id[Validated User Id] for more information. + +[[message-properties]] +== Message Properties + +[[using-correlationid]] +=== Using `CorrelationId` + +The `correlationId` message property can now be a `String`. +See xref:amqp/message-converters.adoc#message-properties-converters[Message Properties Converters] for more information. + +[[long-string-headers]] +=== Long String Headers + +Previously, the `DefaultMessagePropertiesConverter` "`converted`" headers longer than the long string limit (default 1024) +to a `DataInputStream` (actually, it referenced the `LongString` instance's `DataInputStream`). +On output, this header was not converted (except to a String -- for example, `java.io.DataInputStream@1d057a39` by calling +`toString()` on the stream). + +With this release, long `LongString` instances are now left as `LongString` instances by default. +You can access the contents by using the `getBytes[]`, `toString()`, or `getStream()` methods. +A large incoming `LongString` is now correctly "`converted`" on output too. + +See xref:amqp/message-converters.adoc#message-properties-converters[Message Properties Converters] for more information. + +[[inbound-delivery-mode]] +=== Inbound Delivery Mode + +The `deliveryMode` property is no longer mapped to the `MessageProperties.deliveryMode`. +This change avoids unintended propagation if the the same `MessageProperties` object is used to send an outbound message. +Instead, the inbound `deliveryMode` header is mapped to `MessageProperties.receivedDeliveryMode`. + +See xref:amqp/message-converters.adoc#message-properties-converters[Message Properties Converters] for more information. + +When using annotated endpoints, the header is provided in the header named `AmqpHeaders.RECEIVED_DELIVERY_MODE`. + +See xref:amqp/receiving-messages/async-annotation-driven/enable-signature.adoc[Annotated Endpoint Method Signature] for more information. + +[[inbound-user-id]] +=== Inbound User ID + +The `user_id` property is no longer mapped to the `MessageProperties.userId`. +This change avoids unintended propagation if the the same `MessageProperties` object is used to send an outbound message. +Instead, the inbound `userId` header is mapped to `MessageProperties.receivedUserId`. + +See xref:amqp/message-converters.adoc#message-properties-converters[Message Properties Converters] for more information. + +When you use annotated endpoints, the header is provided in the header named `AmqpHeaders.RECEIVED_USER_ID`. + +See xref:amqp/receiving-messages/async-annotation-driven/enable-signature.adoc[Annotated Endpoint Method Signature] for more information. + +[[rabbitadmin-changes]] +== `RabbitAdmin` Changes + +[[declaration-failures]] +=== Declaration Failures + +Previously, the `ignoreDeclarationFailures` flag took effect only for `IOException` on the channel (such as mis-matched +arguments). +It now takes effect for any exception (such as `TimeoutException`). +In addition, a `DeclarationExceptionEvent` is now published whenever a declaration fails. +The `RabbitAdmin` last declaration event is also available as a property `lastDeclarationExceptionEvent`. +See xref:amqp/broker-configuration.adoc[Configuring the Broker] for more information. + +[[rabbitlistener-changes]] +== `@RabbitListener` Changes + +[[multiple-containers-for-each-bean]] +=== Multiple Containers for Each Bean + +When you use Java 8 or later, you can now add multiple `@RabbitListener` annotations to `@Bean` classes or +their methods. +When using Java 7 or earlier, you can use the `@RabbitListeners` container annotation to provide the same +functionality. +See xref:amqp/receiving-messages/async-annotation-driven/repeatable-rabbit-listener.adoc[`@Repeatable` `@RabbitListener`] for more information. + +[[sendto-spel-expressions]] +=== `@SendTo` SpEL Expressions + +`@SendTo` for routing replies with no `replyTo` property can now be SpEL expressions evaluated against the +request/reply. +See xref:amqp/receiving-messages/async-annotation-driven/reply.adoc[Reply Management] for more information. + +[[queuebinding-improvements]] +=== `@QueueBinding` Improvements + +You can now specify arguments for queues, exchanges, and bindings in `@QueueBinding` annotations. +Header exchanges are now supported by `@QueueBinding`. +See xref:amqp/receiving-messages/async-annotation-driven.adoc[Annotation-driven Listener Endpoints] for more information. + +[[delayed-message-exchange]] +== Delayed Message Exchange + +Spring AMQP now has first class support for the RabbitMQ Delayed Message Exchange plugin. +See <> for more information. + +[[exchange-internal-flag]] +== Exchange Internal Flag + +Any `Exchange` definitions can now be marked as `internal`, and `RabbitAdmin` passes the value to the broker when +declaring the exchange. +See xref:amqp/broker-configuration.adoc[Configuring the Broker] for more information. + +[[cachingconnectionfactory-changes]] +== `CachingConnectionFactory` Changes + +[[cachingconnectionfactory-cache-statistics]] +=== `CachingConnectionFactory` Cache Statistics + +The `CachingConnectionFactory` now provides cache properties at runtime and over JMX. +See xref:amqp/connections.adoc#runtime-cache-properties[Runtime Cache Properties] for more information. + +[[accessing-the-underlying-rabbitmq-connection-factory]] +=== Accessing the Underlying RabbitMQ Connection Factory + +A new getter has been added to provide access to the underlying factory. +You can use this getter, for example, to add custom connection properties. +See xref:amqp/custom-client-props.adoc[Adding Custom Client Connection Properties] for more information. + +[[channel-cache]] +=== Channel Cache + +The default channel cache size has been increased from 1 to 25. +See xref:amqp/connections.adoc[Connection and Resource Management] for more information. + +In addition, the `SimpleMessageListenerContainer` no longer adjusts the cache size to be at least as large as the number +of `concurrentConsumers` -- this was superfluous, since the container consumer channels are never cached. + +[[using-rabbitconnectionfactorybean]] +== Using `RabbitConnectionFactoryBean` + +The factory bean now exposes a property to add client connection properties to connections made by the resulting +factory. + +[[java-deserialization]] +== Java Deserialization + +You can now configure a "`allowed list`" of allowable classes when you use Java deserialization. +You should consider creating an allowed list if you accept messages with serialized java objects from +untrusted sources. +See <> for more information. + +[[json-messageconverter]] +== JSON `MessageConverter` + +Improvements to the JSON message converter now allow the consumption of messages that do not have type information +in message headers. +See xref:amqp/receiving-messages/async-annotation-driven/conversion.adoc[Message Conversion for Annotated Methods] and <> for more information. + +[[logging-appenders]] +== Logging Appenders + +[[log4j-2]] +=== Log4j 2 + +A log4j 2 appender has been added, and the appenders can now be configured with an `addresses` property to connect +to a broker cluster. + +[[client-connection-properties]] +=== Client Connection Properties + +You can now add custom client connection properties to RabbitMQ connections. + +See xref:logging.adoc[Logging Subsystem AMQP Appenders] for more information. + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-7-since-1-6.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-7-since-1-6.adoc new file mode 100644 index 0000000000..2c565b3025 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-7-since-1-6.adoc @@ -0,0 +1,76 @@ +[[changes-in-1-7-since-1-6]] += Changes in 1.7 Since 1.6 + +[[amqp-client-library]] +== AMQP Client library + +Spring AMQP now uses the new 4.0.x version of the `amqp-client` library provided by the RabbitMQ team. +This client has auto-recovery configured by default. +See xref:amqp/connections.adoc#auto-recovery[RabbitMQ Automatic Connection/Topology recovery]. + +NOTE: The 4.0.x client enables automatic recovery by default. +While compatible with this feature, Spring AMQP has its own recovery mechanisms, and the client recovery feature generally is not needed. +We recommend disabling `amqp-client` automatic recovery, to avoid getting `AutoRecoverConnectionNotCurrentlyOpenException` instances when the broker is available but the connection has not yet recovered. +Starting with version 1.7.1, Spring AMQP disables it unless you explicitly create your own RabbitMQ connection factory and provide it to the `CachingConnectionFactory`. +RabbitMQ `ConnectionFactory` instances created by the `RabbitConnectionFactoryBean` also have the option disabled by default. + + +[[log4j-2-upgrade]] +== Log4j 2 upgrade +The minimum Log4j 2 version (for the `AmqpAppender`) is now `2.7`. +The framework is no longer compatible with previous versions. +See xref:logging.adoc[Logging Subsystem AMQP Appenders] for more information. + +[[logback-appender]] +== Logback Appender + +This appender no longer captures caller data (method, line number) by default. +You can re-enable it by setting the `includeCallerData` configuration option. +See xref:logging.adoc[Logging Subsystem AMQP Appenders] for information about the available log appenders. + +[[spring-retry-upgrade]] +== Spring Retry Upgrade + +The minimum Spring Retry version is now `1.2`. +The framework is no longer compatible with previous versions. + +[[shutdown-behavior]] +=== Shutdown Behavior + +You can now set `forceCloseChannel` to `true` so that, if the container threads do not respond to a shutdown within `shutdownTimeout`, the channels are forced closed, +causing any unacked messages to be re-queued. +See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration] for more information. + +[[fasterxml-jackson-upgrade]] +== FasterXML Jackson upgrade + +The minimum Jackson version is now `2.8`. +The framework is no longer compatible with previous versions. + +[[junit-rules]] +== JUnit `@Rules` + +Rules that have previously been used internally by the framework have now been made available in a separate jar called `spring-rabbit-junit`. +See <> for more information. + +[[container-conditional-rollback]] +== Container Conditional Rollback + +When you use an external transaction manager (such as JDBC), rule-based rollback is now supported when you provide the container with a transaction attribute. +It is also now more flexible when you use a transaction advice. + +[[connection-naming-strategy]] +== Connection Naming Strategy + +A new `ConnectionNameStrategy` is now provided to populate the application-specific identification of the target RabbitMQ connection from the `AbstractConnectionFactory`. +See xref:amqp/connections.adoc[Connection and Resource Management] for more information. + +[[listener-container-changes]] +== Listener Container Changes + +[[transaction-rollback-behavior]] +=== Transaction Rollback Behavior + +You can now configure message re-queue on transaction rollback to be consistent, regardless of whether or not a transaction manager is configured. +See xref:amqp/transactions.adoc#transaction-rollback[A note on Rollback of Received Messages] for more information. + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-0-since-1-7.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-0-since-1-7.adoc new file mode 100644 index 0000000000..9355130498 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-0-since-1-7.adoc @@ -0,0 +1,203 @@ +[[changes-in-2-0-since-1-7]] += Changes in 2.0 Since 1.7 + +[[using-cachingconnectionfactory]] +== Using `CachingConnectionFactory` + +Starting with version 2.0.2, you can configure the `RabbitTemplate` to use a different connection to that used by listener containers. +This change avoids deadlocked consumers when producers are blocked for any reason. +See xref:amqp/template.adoc#separate-connection[Using a Separate Connection] for more information. + +[[amqp-client-library]] +== AMQP Client library + +Spring AMQP now uses the new 5.0.x version of the `amqp-client` library provided by the RabbitMQ team. +This client has auto recovery configured by default. +See xref:amqp/connections.adoc#auto-recovery[RabbitMQ Automatic Connection/Topology recovery]. + +NOTE: As of version 4.0, the client enables automatic recovery by default. +While compatible with this feature, Spring AMQP has its own recovery mechanisms, and the client recovery feature generally is not needed. +We recommend that you disable `amqp-client` automatic recovery, to avoid getting `AutoRecoverConnectionNotCurrentlyOpenException` instances when the broker is available but the connection has not yet recovered. +Starting with version 1.7.1, Spring AMQP disables it unless you explicitly create your own RabbitMQ connection factory and provide it to the `CachingConnectionFactory`. +RabbitMQ `ConnectionFactory` instances created by the `RabbitConnectionFactoryBean` also have the option disabled by default. + +[[general-changes]] +== General Changes + +The `ExchangeBuilder` now builds durable exchanges by default. +The `@Exchange` annotation used within a `@QeueueBinding` also declares durable exchanges by default. +The `@Queue` annotation used within a `@RabbitListener` by default declares durable queues if named and non-durable if anonymous. +See xref:amqp/broker-configuration.adoc#builder-api[Builder API for Queues and Exchanges] and xref:amqp/receiving-messages/async-annotation-driven.adoc[Annotation-driven Listener Endpoints] for more information. + +[[deleted-classes]] +== Deleted Classes + +`UniquelyNameQueue` is no longer provided. +It is unusual to create a durable non-auto-delete queue with a unique name. +This class has been deleted. +If you require its functionality, use `new Queue(UUID.randomUUID().toString())`. + +[[new-listener-container]] +== New Listener Container + +The `DirectMessageListenerContainer` has been added alongside the existing `SimpleMessageListenerContainer`. +See xref:amqp/receiving-messages/choose-container.adoc[Choosing a Container] and xref:amqp/containerAttributes.adoc[Message Listener Container Configuration] for information about choosing which container to use as well as how to configure them. + + +[[log4j-appender]] +== Log4j Appender + +This appender is no longer available due to the end-of-life of log4j. +See xref:logging.adoc[Logging Subsystem AMQP Appenders] for information about the available log appenders. + + +[[rabbittemplate-changes]] +== `RabbitTemplate` Changes + +IMPORTANT: Previously, a non-transactional `RabbitTemplate` participated in an existing transaction if it ran on a transactional listener container thread. +This was a serious bug. +However, users might have relied on this behavior. +Starting with version 1.6.2, you must set the `channelTransacted` boolean on the template for it to participate in the container transaction. + +The `RabbitTemplate` now uses a `DirectReplyToMessageListenerContainer` (by default) instead of creating a new consumer for each request. +See xref:amqp/request-reply.adoc#direct-reply-to[RabbitMQ Direct reply-to] for more information. + +The `AsyncRabbitTemplate` now supports direct reply-to. +See xref:amqp/request-reply.adoc#async-template[Async Rabbit Template] for more information. + +The `RabbitTemplate` and `AsyncRabbitTemplate` now have `receiveAndConvert` and `convertSendAndReceiveAsType` methods that take a `ParameterizedTypeReference` argument, letting the caller specify the type to which to convert the result. +This is particularly useful for complex types or when type information is not conveyed in message headers. +It requires a `SmartMessageConverter` such as the `Jackson2JsonMessageConverter`. +See xref:amqp/request-reply.adoc[Request/Reply Messaging], xref:amqp/request-reply.adoc#async-template[Async Rabbit Template], xref:amqp/message-converters.adoc#json-complex[Converting From a `Message` With `RabbitTemplate`], and <> for more information. + +You can now use a `RabbitTemplate` to perform multiple operations on a dedicated channel. +See xref:amqp/template.adoc#scoped-operations[Scoped Operations] for more information. + +[[listener-adapter]] +== Listener Adapter + +A convenient `FunctionalInterface` is available for using lambdas with the `MessageListenerAdapter`. +See xref:amqp/receiving-messages/async-consumer.adoc#message-listener-adapter[`MessageListenerAdapter`] for more information. + +[[listener-container-changes]] +== Listener Container Changes + +[[prefetch-default-value]] +=== Prefetch Default Value + +The prefetch default value used to be 1, which could lead to under-utilization of efficient consumers. +The default prefetch value is now 250, which should keep consumers busy in most common scenarios and, +thus, improve throughput. + +IMPORTANT: There are scenarios where the prefetch value should +be low -- for example, with large messages, especially if the processing is slow (messages could add up +to a large amount of memory in the client process), and if strict message ordering is necessary +(the prefetch value should be set back to 1 in this case). +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. + +For more background about prefetch, see this post about https://www.rabbitmq.com/blog/2014/04/14/finding-bottlenecks-with-rabbitmq-3-3/[consumer utilization in RabbitMQ] +and this post about https://www.rabbitmq.com/blog/2012/05/11/some-queuing-theory-throughput-latency-and-bandwidth/[queuing theory]. + +[[message-count]] +=== Message Count + +Previously, `MessageProperties.getMessageCount()` returned `0` for messages emitted by the container. +This property applies only when you use `basicGet` (for example, from `RabbitTemplate.receive()` methods) and is now initialized to `null` for container messages. + +[[transaction-rollback-behavior]] +=== Transaction Rollback Behavior + +Message re-queue on transaction rollback is now consistent, regardless of whether or not a transaction manager is configured. +See xref:amqp/transactions.adoc#transaction-rollback[A note on Rollback of Received Messages] for more information. + +[[shutdown-behavior]] +=== Shutdown Behavior + +If the container threads do not respond to a shutdown within `shutdownTimeout`, the channels are forced closed by default. +See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration] for more information. + +[[after-receive-message-post-processors]] +=== After Receive Message Post Processors + +If a `MessagePostProcessor` in the `afterReceiveMessagePostProcessors` property returns `null`, the message is discarded (and acknowledged if appropriate). + +[[connection-factory-changes]] +== Connection Factory Changes + +The connection and channel listener interfaces now provide a mechanism to obtain information about exceptions. +See xref:amqp/connections.adoc#connection-channel-listeners[Connection and Channel Listeners] and xref:amqp/template.adoc#publishing-is-async[Publishing is Asynchronous -- How to Detect Successes and Failures] for more information. + +A new `ConnectionNameStrategy` is now provided to populate the application-specific identification of the target RabbitMQ connection from the `AbstractConnectionFactory`. +See xref:amqp/connections.adoc[Connection and Resource Management] for more information. + +[[retry-changes]] +== Retry Changes + +The `MissingMessageIdAdvice` is no longer provided. +Its functionality is now built-in. +See xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc#retry[Failures in Synchronous Operations and Options for Retry] for more information. + +[[anonymous-queue-naming]] +== Anonymous Queue Naming + +By default, `AnonymousQueues` are now named with the default `Base64UrlNamingStrategy` instead of a simple `UUID` string. +See xref:amqp/broker-configuration.adoc#anonymous-queue[`AnonymousQueue`] for more information. + +[[rabbitlistener-changes]] +== `@RabbitListener` Changes + +You can now provide simple queue declarations (bound only to the default exchange) in `@RabbitListener` annotations. +See xref:amqp/receiving-messages/async-annotation-driven.adoc[Annotation-driven Listener Endpoints] for more information. + +You can now configure `@RabbitListener` annotations so that any exceptions are returned to the sender. +You can also configure a `RabbitListenerErrorHandler` to handle exceptions. +See xref:amqp/receiving-messages/async-annotation-driven/error-handling.adoc[Handling Exceptions] for more information. + +You can now bind a queue with multiple routing keys when you use the `@QueueBinding` annotation. +Also `@QueueBinding.exchange()` now supports custom exchange types and declares durable exchanges by default. + +You can now set the `concurrency` of the listener container at the annotation level rather than having to configure a different container factory for different concurrency settings. + +You can now set the `autoStartup` property of the listener container at the annotation level, overriding the default setting in the container factory. + +You can now set after receive and before send (reply) `MessagePostProcessor` instances in the `RabbitListener` container factories. + +See xref:amqp/receiving-messages/async-annotation-driven.adoc[Annotation-driven Listener Endpoints] for more information. + +Starting with version 2.0.3, one of the `@RabbitHandler` annotations on a class-level `@RabbitListener` can be designated as the default. +See xref:amqp/receiving-messages/async-annotation-driven/method-selection.adoc[Multi-method Listeners] for more information. + +[[container-conditional-rollback]] +== Container Conditional Rollback + +When using an external transaction manager (such as JDBC), rule-based rollback is now supported when you provide the container with a transaction attribute. +It is also now more flexible when you use a transaction advice. +See xref:amqp/transactions.adoc#conditional-rollback[Conditional Rollback] for more information. + +[[remove-jackson-1-x-support]] +== Remove Jackson 1.x support + +Deprecated in previous versions, Jackson `1.x` converters and related components have now been deleted. +You can use similar components based on Jackson 2.x. +See <> for more information. + +[[json-message-converter]] +== JSON Message Converter + +When the `__TypeId__` is set to `Hashtable` for an inbound JSON message, the default conversion type is now `LinkedHashMap`. +Previously, it was `Hashtable`. +To revert to a `Hashtable`, you can use `setDefaultMapType` on the `DefaultClassMapper`. + +[[xml-parsers]] +== XML Parsers + +When parsing `Queue` and `Exchange` XML components, the parsers no longer register the `name` attribute value as a bean alias if an `id` attribute is present. +See xref:amqp/broker-configuration.adoc#note-id-name[A Note On the `id` and `name` Attributes] for more information. + +[[blocked-connection]] +== Blocked Connection +You can now inject the `com.rabbitmq.client.BlockedListener` into the `org.springframework.amqp.rabbit.connection.Connection` object. +Also, the `ConnectionBlockedEvent` and `ConnectionUnblockedEvent` events are emitted by the `ConnectionFactory` when the connection is blocked or unblocked by the Broker. + +See xref:amqp/connections.adoc[Connection and Resource Management] for more information. + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-1-since-2-0.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-1-since-2-0.adoc new file mode 100644 index 0000000000..a905c1899b --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-1-since-2-0.adoc @@ -0,0 +1,127 @@ +[[changes-in-2-1-since-2-0]] += Changes in 2.1 Since 2.0 + +[[amqp-client-library]] +== AMQP Client library + +Spring AMQP now uses the 5.4.x version of the `amqp-client` library provided by the RabbitMQ team. +This client has auto-recovery configured by default. +See xref:amqp/connections.adoc#auto-recovery[RabbitMQ Automatic Connection/Topology recovery]. + +NOTE: As of version 4.0, the client enables automatic recovery by default. +While compatible with this feature, Spring AMQP has its own recovery mechanisms and the client recovery feature generally is not needed. +We recommend disabling `amqp-client` automatic recovery, to avoid getting `AutoRecoverConnectionNotCurrentlyOpenException` instances when the broker is available but the connection has not yet recovered. +Starting with version 1.7.1, Spring AMQP disables it unless you explicitly create your own RabbitMQ connection factory and provide it to the `CachingConnectionFactory`. +RabbitMQ `ConnectionFactory` instances created by the `RabbitConnectionFactoryBean` also have the option disabled by default. + + +[[package-changes]] +== Package Changes + +Certain classes have moved to different packages. +Most are internal classes and do not affect user applications. +Two exceptions are `ChannelAwareMessageListener` and `RabbitListenerErrorHandler`. +These interfaces are now in `org.springframework.amqp.rabbit.listener.api`. + +[[publisher-confirms-changes]] +== Publisher Confirms Changes + +Channels enabled for publisher confirmations are not returned to the cache while there are outstanding confirmations. +See xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns] for more information. + +[[listener-container-factory-improvements]] +== Listener Container Factory Improvements + +You can now use the listener container factories to create any listener container, not only those for use with `@RabbitListener` annotations or the `@RabbitListenerEndpointRegistry`. +See xref:amqp/receiving-messages/using-container-factories.adoc[Using Container Factories] for more information. + +`ChannelAwareMessageListener` now inherits from `MessageListener`. + +[[broker-event-listener]] +== Broker Event Listener + +A `BrokerEventListener` is introduced to publish selected broker events as `ApplicationEvent` instances. +See xref:amqp/broker-events.adoc[Broker Event Listener] for more information. + +[[rabbitadmin-changes]] +== RabbitAdmin Changes + +The `RabbitAdmin` discovers beans of type `Declarables` (which is a container for `Declarable` - `Queue`, `Exchange`, and `Binding` objects) and declare the contained objects on the broker. +Users are discouraged from using the old mechanism of declaring `>` (and others) and should use `Declarables` beans instead. +By default, the old mechanism is disabled. +See xref:amqp/broker-configuration.adoc#collection-declaration[Declaring Collections of Exchanges, Queues, and Bindings] for more information. + +`AnonymousQueue` instances are now declared with `x-queue-master-locator` set to `client-local` by default, to ensure the queues are created on the node the application is connected to. +See xref:amqp/broker-configuration.adoc[Configuring the Broker] for more information. + +[[rabbittemplate-changes]] +== RabbitTemplate Changes + +You can now configure the `RabbitTemplate` with the `noLocalReplyConsumer` option to control a `noLocal` flag for reply consumers in the `sendAndReceive()` operations. +See xref:amqp/request-reply.adoc[Request/Reply Messaging] for more information. + +`CorrelationData` for publisher confirmations now has a `ListenableFuture`, which you can use to get the acknowledgment instead of using a callback. +When returns and confirmations are enabled, the correlation data, if provided, is populated with the returned message. +See xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns] for more information. + +A method called `replyTimedOut` is now provided to notify subclasses that a reply has timed out, allowing for any state cleanup. +See xref:amqp/request-reply.adoc#reply-timeout[Reply Timeout] for more information. + +You can now specify an `ErrorHandler` to be invoked when using request/reply with a `DirectReplyToMessageListenerContainer` (the default) when exceptions occur when replies are delivered (for example, late replies). +See `setReplyErrorHandler` on the `RabbitTemplate`. +(Also since 2.0.11). + +[[message-conversion]] +== Message Conversion + +We introduced a new `Jackson2XmlMessageConverter` to support converting messages from and to XML format. +See xref:amqp/message-converters.adoc#jackson2xml[`Jackson2XmlMessageConverter`] for more information. + +[[management-rest-api]] +== Management REST API + +The `RabbitManagementTemplate` is now deprecated in favor of the direct `com.rabbitmq.http.client.Client` (or `com.rabbitmq.http.client.ReactorNettyClient`) usage. +See <> for more information. + +[[rabbitlistener-changes]] +== `@RabbitListener` Changes + +The listener container factory can now be configured with a `RetryTemplate` and, optionally, a `RecoveryCallback` used when sending replies. +See xref:amqp/receiving-messages/async-annotation-driven/enable.adoc[Enable Listener Endpoint Annotations] for more information. + +[[async-rabbitlistener-return]] +== Async `@RabbitListener` Return + +`@RabbitListener` methods can now return `ListenableFuture` or `Mono`. +See xref:amqp/receiving-messages/async-returns.adoc[Asynchronous `@RabbitListener` Return Types] for more information. + +[[connection-factory-bean-changes]] +== Connection Factory Bean Changes + +By default, the `RabbitConnectionFactoryBean` now calls `enableHostnameVerification()`. +To revert to the previous behavior, set the `enableHostnameVerification` property to `false`. + +[[connection-factory-changes]] +== Connection Factory Changes + +The `CachingConnectionFactory` now unconditionally disables auto-recovery in the underlying RabbitMQ `ConnectionFactory`, even if a pre-configured instance is provided in a constructor. +While steps have been taken to make Spring AMQP compatible with auto recovery, certain corner cases have arisen where issues remain. +Spring AMQP has had its own recovery mechanism since 1.0.0 and does not need to use the recovery provided by the client. +While it is still possible to enable the feature (using `cachingConnectionFactory.getRabbitConnectionFactory()` `.setAutomaticRecoveryEnabled()`) after the `CachingConnectionFactory` is constructed, **we strongly recommend that you not do so**. +We recommend that you use a separate RabbitMQ `ConnectionFactory` if you need auto recovery connections when using the client factory directly (rather than using Spring AMQP components). + +[[listener-container-changes]] +== Listener Container Changes + +The default `ConditionalRejectingErrorHandler` now completely discards messages that cause fatal errors if an `x-death` header is present. +See xref:amqp/exception-handling.adoc[Exception Handling] for more information. + +[[immediate-requeue]] +== Immediate requeue + +A new `ImmediateRequeueAmqpException` is introduced to notify a listener container that the message has to be re-queued. +To use this feature, a new `ImmediateRequeueMessageRecoverer` implementation is added. + +See xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc#async-listeners[Message Listeners and the Asynchronous Case] for more information. + + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-2-since-2-1.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-2-since-2-1.adoc new file mode 100644 index 0000000000..90e38330b8 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-2-since-2-1.adoc @@ -0,0 +1,137 @@ +[[changes-in-2-2-since-2-1]] += Changes in 2.2 Since 2.1 + +This section describes the changes between version 2.1 and version 2.2. + +[[package-changes]] +== Package Changes + +The following classes/interfaces have been moved from `org.springframework.amqp.rabbit.core.support` to `org.springframework.amqp.rabbit.batch`: + +* `BatchingStrategy` +* `MessageBatch` +* `SimpleBatchingStrategy` + +In addition, `ListenerExecutionFailedException` has been moved from `org.springframework.amqp.rabbit.listener.exception` to `org.springframework.amqp.rabbit.support`. + +[[dependency-changes]] +== Dependency Changes + +JUnit (4) is now an optional dependency and will no longer appear as a transitive dependency. + +The `spring-rabbit-junit` module is now a *compile* dependency in the `spring-rabbit-test` module for a better target application development experience when with only a single `spring-rabbit-test` we get the full stack of testing utilities for AMQP components. + +[[-breaking-api-changes]] +== "Breaking" API Changes + +the JUnit (5) `RabbitAvailableCondition.getBrokerRunning()` now returns a `BrokerRunningSupport` instance instead of a `BrokerRunning`, which depends on JUnit 4. +It has the same API so it's just a matter of changing the class name of any references. +See xref:testing.adoc#junit5-conditions[JUnit5 Conditions] for more information. + +[[listenercontainer-changes]] +== ListenerContainer Changes + +Messages with fatal exceptions are now rejected and NOT requeued, by default, even if the acknowledge mode is manual. +See xref:amqp/exception-handling.adoc[Exception Handling] for more information. + +Listener performance can now be monitored using Micrometer `Timer` s. +See xref:amqp/receiving-messages/micrometer.adoc[Monitoring Listener Performance] for more information. + +[[rabbitlistener-changes]] +== @RabbitListener Changes + +You can now configure an `executor` on each listener, overriding the factory configuration, to more easily identify threads associated with the listener. +You can now override the container factory's `acknowledgeMode` property with the annotation's `ackMode` property. +See xref:amqp/receiving-messages/async-annotation-driven/enable.adoc#listener-property-overrides[overriding container factory properties] for more information. + +When using xref:amqp/receiving-messages/batch.adoc[batching], `@RabbitListener` methods can now receive a complete batch of messages in one call instead of getting them one-at-a-time. + +When receiving batched messages one-at-a-time, the last message has the `isLastInBatch` message property set to true. + +In addition, received batched messages now contain the `amqp_batchSize` header. + +Listeners can also consume batches created in the `SimpleMessageListenerContainer`, even if the batch is not created by the producer. +See xref:amqp/receiving-messages/choose-container.adoc[Choosing a Container] for more information. + +Spring Data Projection interfaces are now supported by the `Jackson2JsonMessageConverter`. +See xref:amqp/message-converters.adoc#data-projection[Using Spring Data Projection Interfaces] for more information. + +The `Jackson2JsonMessageConverter` now assumes the content is JSON if there is no `contentType` property, or it is the default (`application/octet-string`). +See xref:amqp/message-converters.adoc#Jackson2JsonMessageConverter-from-message[Converting from a `Message`] for more information. + +Similarly. the `Jackson2XmlMessageConverter` now assumes the content is XML if there is no `contentType` property, or it is the default (`application/octet-string`). +See xref:amqp/message-converters.adoc#jackson2xml[`Jackson2XmlMessageConverter`] for more information. + +When a `@RabbitListener` method returns a result, the bean and `Method` are now available in the reply message properties. +This allows configuration of a `beforeSendReplyMessagePostProcessor` to, for example, set a header in the reply to indicate which method was invoked on the server. +See xref:amqp/receiving-messages/async-annotation-driven/reply.adoc[Reply Management] for more information. + +You can now configure a `ReplyPostProcessor` to make modifications to a reply message before it is sent. +See xref:amqp/receiving-messages/async-annotation-driven/reply.adoc[Reply Management] for more information. + +[[amqp-logging-appenders-changes]] +== AMQP Logging Appenders Changes + +The Log4J and Logback `AmqpAppender` s now support a `verifyHostname` SSL option. + +Also these appenders now can be configured to not add MDC entries as headers. +The `addMdcAsHeaders` boolean option has been introduces to configure such a behavior. + +The appenders now support the `SaslConfig` property. + +See xref:logging.adoc[Logging Subsystem AMQP Appenders] for more information. + +[[messagelisteneradapter-changes]] +== MessageListenerAdapter Changes + +The `MessageListenerAdapter` provides now a new `buildListenerArguments(Object, Channel, Message)` method to build an array of arguments to be passed into target listener and an old one is deprecated. +See xref:amqp/receiving-messages/async-consumer.adoc#message-listener-adapter[`MessageListenerAdapter`] for more information. + +[[exchange/queue-declaration-changes]] +== Exchange/Queue Declaration Changes + +The `ExchangeBuilder` and `QueueBuilder` fluent APIs used to create `Exchange` and `Queue` objects for declaration by `RabbitAdmin` now support "well known" arguments. +See xref:amqp/broker-configuration.adoc#builder-api[Builder API for Queues and Exchanges] for more information. + +The `RabbitAdmin` has a new property `explicitDeclarationsOnly`. +See xref:amqp/broker-configuration.adoc#conditional-declaration[Conditional Declaration] for more information. + +[[connection-factory-changes]] +== Connection Factory Changes + +The `CachingConnectionFactory` has a new property `shuffleAddresses`. +When providing a list of broker node addresses, the list will be shuffled before creating a connection so that the order in which the connections are attempted is random. +See xref:amqp/connections.adoc#cluster[Connecting to a Cluster] for more information. + +When using Publisher confirms and returns, the callbacks are now invoked on the connection factory's `executor`. +This avoids a possible deadlock in the `amqp-clients` library if you perform rabbit operations from within the callback. +See xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns] for more information. + +Also, the publisher confirm type is now specified with the `ConfirmType` enum instead of the two mutually exclusive setter methods. + +The `RabbitConnectionFactoryBean` now uses TLS 1.2 by default when SSL is enabled. +See xref:amqp/connections.adoc#rabbitconnectionfactorybean-configuring-ssl[`RabbitConnectionFactoryBean` and Configuring SSL] for more information. + +[[new-messagepostprocessor-classes]] +== New MessagePostProcessor Classes + +Classes `DeflaterPostProcessor` and `InflaterPostProcessor` were added to support compression and decompression, respectively, when the message content-encoding is set to `deflate`. + +[[other-changes]] +== Other Changes + +The `Declarables` object (for declaring multiple queues, exchanges, bindings) now has a filtered getter for each type. +See xref:amqp/broker-configuration.adoc#collection-declaration[Declaring Collections of Exchanges, Queues, and Bindings] for more information. + +You can now customize each `Declarable` bean before the `RabbitAdmin` processes the declaration thereof. +See xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc#automatic-declaration[Automatic Declaration of Exchanges, Queues, and Bindings] for more information. + +`singleActiveConsumer()` has been added to the `QueueBuilder` to set the `x-single-active-consumer` queue argument. +See xref:amqp/broker-configuration.adoc#builder-api[Builder API for Queues and Exchanges] for more information. + +Outbound headers with values of type `Class` are now mapped using `getName()` instead of `toString()`. +See xref:amqp/message-converters.adoc#message-properties-converters[Message Properties Converters] for more information. + +Recovery of failed producer-created batches is now supported. +See xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc#batch-retry[Retry with Batch Listeners] for more information. + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-3-since-2-2.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-3-since-2-2.adoc new file mode 100644 index 0000000000..d49bf3128f --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-3-since-2-2.adoc @@ -0,0 +1,72 @@ +[[changes-in-2-3-since-2-2]] += Changes in 2.3 Since 2.2 + +This section describes the changes between version 2.2 and version 2.3. +See xref:appendix/change-history.adoc[Change History] for changes in previous versions. + +[[connection-factory-changes]] +== Connection Factory Changes + +Two additional connection factories are now provided. +See xref:amqp/connections.adoc#choosing-factory[Choosing a Connection Factory] for more information. + +[[rabbitlistener-changes]] +== `@RabbitListener` Changes + +You can now specify a reply content type. +See xref:amqp/receiving-messages/async-annotation-driven/reply-content-type.adoc[Reply ContentType] for more information. + +[[message-converter-changes]] +== Message Converter Changes + +The `Jackson2JMessageConverter` s can now deserialize abstract classes (including interfaces) if the `ObjectMapper` is configured with a custom deserializer. +See xref:amqp/message-converters.adoc#jackson-abstract[Deserializing Abstract Classes] for more information. + +[[testing-changes]] +== Testing Changes + +A new annotation `@SpringRabbitTest` is provided to automatically configure some infrastructure beans for when you are not using `SpringBootTest`. +See xref:testing.adoc#spring-rabbit-test[@SpringRabbitTest] for more information. + +[[rabbittemplate-changes]] +== RabbitTemplate Changes + +The template's `ReturnCallback` has been refactored as `ReturnsCallback` for simpler use in lambda expressions. +See xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns] for more information. + +When using returns and correlated confirms, the `CorrelationData` now requires a unique `id` property. +See xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns] for more information. + +When using direct reply-to, you can now configure the template such that the server does not need to return correlation data with the reply. +See xref:amqp/request-reply.adoc#direct-reply-to[RabbitMQ Direct reply-to] for more information. + +[[listener-container-changes]] +== Listener Container Changes + +A new listener container property `consumeDelay` is now available; it is helpful when using the https://github.com/rabbitmq/rabbitmq-sharding[RabbitMQ Sharding Plugin]. + +The default `JavaLangErrorHandler` now calls `System.exit(99)`. +To revert to the previous behavior (do nothing), add a no-op handler. + +The containers now support the `globalQos` property to apply the `prefetchCount` globally for the channel rather than for each consumer on the channel. + +See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration] for more information. + +[[messagepostprocessor-changes]] +== MessagePostProcessor Changes + +The compressing `MessagePostProcessor` s now use a comma to separate multiple content encodings instead of a colon. +The decompressors can handle both formats but, if you produce messages with this version that are consumed by versions earlier than 2.2.12, you should configure the compressor to use the old delimiter. +See the IMPORTANT note in xref:amqp/post-processing.adoc[Modifying Messages - Compression and More] for more information. + +[[multiple-broker-support-improvements]] +== Multiple Broker Support Improvements + +See xref:amqp/multi-rabbit.adoc[Multiple Broker (or Cluster) Support] for more information. + +[[republishmessagerecoverer-changes]] +== RepublishMessageRecoverer Changes + +A new subclass of this recoverer is not provided that supports publisher confirms. +See xref:amqp/resilience-recovering-from-errors-and-broker-failures.adoc#async-listeners[Message Listeners and the Asynchronous Case] for more information. + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-4-since-2-3.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-4-since-2-3.adoc new file mode 100644 index 0000000000..0dc4bf3fcc --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-4-since-2-3.adoc @@ -0,0 +1,24 @@ +[[changes-in-2-4-since-2-3]] += Changes in 2.4 Since 2.3 +:page-section-summary-toc: 1 + +This section describes the changes between version 2.3 and version 2.4. +See xref:appendix/change-history.adoc[Change History] for changes in previous versions. + +[[rabbitlistener-changes]] +== `@RabbitListener` Changes + +`MessageProperties` is now available for argument matching. +See xref:amqp/receiving-messages/async-annotation-driven/enable-signature.adoc[Annotated Endpoint Method Signature] for more information. + +[[rabbitadmin-changes]] +== `RabbitAdmin` Changes + +A new property `recoverManualDeclarations` allows recovery of manually declared queues/exchanges/bindings. +See xref:amqp/broker-configuration.adoc#declarable-recovery[Recovering Auto-Delete Declarations] for more information. + +[[remoting-support]] +== Remoting Support + +Support remoting using Spring Framework’s RMI support is deprecated and will be removed in 3.0. See Spring Remoting with AMQP for more information. + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-3-0-since-2-4.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-3-0-since-2-4.adoc new file mode 100644 index 0000000000..63562059c8 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-3-0-since-2-4.adoc @@ -0,0 +1,70 @@ +[[changes-in-3-0-since-2-4]] += Changes in 3.0 Since 2.4 + +[[java-17-spring-framework-6-0]] +== Java 17, Spring Framework 6.0 + +This version requires Spring Framework 6.0 and Java 17 + +[[remoting]] +== Remoting + +The remoting feature (using RMI) is no longer supported. + +[[observation]] +== Observation + +Enabling observation for timers and tracing using Micrometer is now supported. +See xref:stream.adoc#stream-micrometer-observation[Micrometer Observation] for more information. + +[[x30-Native]] +== Native Images + +Support for creating native images is provided. +See xref:appendix/previous-whats-new/changes-in-3-0-since-2-4.adoc#x30-Native[Native Images] for more information. + +[[asyncrabbittemplate]] +== AsyncRabbitTemplate + +IMPORTANT: The `AsyncRabbitTemplate` now returns `CompletableFuture` s instead of `ListenableFuture` s. +See xref:amqp/request-reply.adoc#async-template[Async Rabbit Template] for more information. + +[[stream-support-changes]] +== Stream Support Changes + +IMPORTANT: `RabbitStreamOperations` and `RabbitStreamTemplate` methods now return `CompletableFuture` instead of `ListenableFuture`. + +Super streams and single active consumers thereon are now supported. + +See xref:stream.adoc[Using the RabbitMQ Stream Plugin] for more information. + +[[rabbitlistener-changes]] +== `@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 xref:amqp/receiving-messages/batch.adoc[@RabbitListener with Batching] for more information. + +`MessageConverter` s can now return `Optional.empty()` for a null value; this is currently implemented by the `Jackson2JsonMessageConverter`. +See xref:amqp/message-converters.adoc#Jackson2JsonMessageConverter-from-message[Converting from a `Message`] for more information + +You can now configure a `ReplyPostProcessor` via the container factory rather than via a property on `@RabbitListener`. +See xref:amqp/receiving-messages/async-annotation-driven/reply.adoc[Reply Management] for more information. + +The `@RabbitListener` (and `@RabbitHandler`) methods can now be declared as Kotlin `suspend` functions. +See xref:amqp/receiving-messages/async-returns.adoc[Asynchronous `@RabbitListener` Return Types] for more information. + +Starting with version 3.0.5, listeners with async return types (including Kotlin suspend functions) invoke the `RabbitListenerErrorHandler` (if configured) after a failure. +Previously, the error handler was only invoked with synchronous invocations. + +[[connection-factory-changes]] +== Connection Factory Changes + +The default `addressShuffleMode` in `AbstractConnectionFactory` is now `RANDOM`. +This results in connecting to a random host when multiple addresses are provided. +See xref:amqp/connections.adoc#cluster[Connecting to a Cluster] for more information. + +The `LocalizedQueueConnectionFactory` no longer uses the RabbitMQ `http-client` library to determine which node is the leader for a queue. +See xref:amqp/connections.adoc#queue-affinity[Queue Affinity and the `LocalizedQueueConnectionFactory`] for more information. + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-to-1-1-since-1-0.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-to-1-1-since-1-0.adoc new file mode 100644 index 0000000000..b74f52de59 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-to-1-1-since-1-0.adoc @@ -0,0 +1,21 @@ +[[changes-to-1-1-since-1-0]] += Changes to 1.1 Since 1.0 +:page-section-summary-toc: 1 + +[[general]] +== General + +Spring-AMQP is now built with Gradle. + +Adds support for publisher confirms and returns. + +Adds support for HA queues and broker failover. + +Adds support for dead letter exchanges and dead letter queues. + +[[amqp-log4j-appender]] +== AMQP Log4j Appender + +Adds an option to support adding a message ID to logged messages. + +Adds an option to allow the specification of a `Charset` name to be used when converting `String` to `byte[]`. diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-to-1-2-since-1-1.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-to-1-2-since-1-1.adoc new file mode 100644 index 0000000000..17410e9823 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-to-1-2-since-1-1.adoc @@ -0,0 +1,58 @@ +[[changes-to-1-2-since-1-1]] += Changes to 1.2 Since 1.1 + +[[rabbitmq-version]] +== RabbitMQ Version + +Spring AMQP now uses RabbitMQ 3.1.x by default (but retains compatibility with earlier versions). +Certain deprecations have been added for features no longer supported by RabbitMQ 3.1.x -- federated exchanges and the `immediate` property on the `RabbitTemplate`. + +[[rabbit-admin]] +== Rabbit Admin + +`RabbitAdmin` now provides an option to let exchange, queue, and binding declarations continue when a declaration fails. +Previously, all declarations stopped on a failure. +By setting `ignore-declaration-exceptions`, such exceptions are logged (at the `WARN` level), but further declarations continue. +An example where this might be useful is when a queue declaration fails because of a slightly different `ttl` setting that would normally stop other declarations from proceeding. + +`RabbitAdmin` now provides an additional method called `getQueueProperties()`. +You can use this determine if a queue exists on the broker (returns `null` for a non-existent queue). +In addition, it returns the current number of messages in the queue as well as the current number of consumers. + +[[rabbit-template]] +== Rabbit Template + +Previously, when the `...sendAndReceive()` methods were used with a fixed reply queue, two custom headers were used for correlation data and to retain and restore reply queue information. +With this release, the standard message property (`correlationId`) is used by default, although you can specify a custom property to use instead. +In addition, nested `replyTo` information is now retained internally in the template, instead of using a custom header. + +The `immediate` property is deprecated. +You must not set this property when using RabbitMQ 3.0.x or greater. + +[[json-message-converters]] +== JSON Message Converters + +A Jackson 2.x `MessageConverter` is now provided, along with the existing converter that uses Jackson 1.x. + +[[automatic-declaration-of-queues-and-other-items]] +== Automatic Declaration of Queues and Other Items + +Previously, when declaring queues, exchanges and bindings, you could not define which connection factory was used for the declarations. +Each `RabbitAdmin` declared all components by using its connection. + +Starting with this release, you can now limit declarations to specific `RabbitAdmin` instances. +See xref:amqp/broker-configuration.adoc#conditional-declaration[Conditional Declaration]. + +[[amqp-remoting]] +== AMQP Remoting + +Facilities are now provided for using Spring remoting techniques, using AMQP as the transport for the RPC calls. +For more information see <> + +[[requested-heart-beats]] +== Requested Heart Beats + +Several users have asked for the underlying client connection factory's `requestedHeartBeats` property to be exposed on the Spring AMQP `CachingConnectionFactory`. +This is now available. +Previously, it was necessary to configure the AMQP client factory as a separate bean and provide a reference to it in the `CachingConnectionFactory`. + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/earlier-releases.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/earlier-releases.adoc new file mode 100644 index 0000000000..d92f5b10f6 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/earlier-releases.adoc @@ -0,0 +1,6 @@ +[[earlier-releases]] += Earlier Releases +:page-section-summary-toc: 1 + +See xref:appendix/previous-whats-new.adoc[Previous Releases] for changes in previous versions. + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/message-converter-changes-1.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/message-converter-changes-1.adoc new file mode 100644 index 0000000000..a342746ba4 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/message-converter-changes-1.adoc @@ -0,0 +1,7 @@ +[[message-converter-changes]] += Message Converter Changes +:page-section-summary-toc: 1 + +The `Jackson2JsonMessageConverter` can now determine the charset from the `contentEncoding` header. +See <> for more information. + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/message-converter-changes.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/message-converter-changes.adoc new file mode 100644 index 0000000000..a342746ba4 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/message-converter-changes.adoc @@ -0,0 +1,7 @@ +[[message-converter-changes]] += Message Converter Changes +:page-section-summary-toc: 1 + +The `Jackson2JsonMessageConverter` can now determine the charset from the `contentEncoding` header. +See <> for more information. + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/stream-support-changes.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/stream-support-changes.adoc new file mode 100644 index 0000000000..b6be8ec44a --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/stream-support-changes.adoc @@ -0,0 +1,7 @@ +[[stream-support-changes]] += Stream Support Changes +:page-section-summary-toc: 1 + +`RabbitStreamOperations` and `RabbitStreamTemplate` have been deprecated in favor of `RabbitStreamOperations2` and `RabbitStreamTemplate2` respectively; they return `CompletableFuture` instead of `ListenableFuture`. +See xref:stream.adoc[Using the RabbitMQ Stream Plugin] for more information. + diff --git a/src/reference/asciidoc/further-reading.adoc b/src/reference/antora/modules/ROOT/pages/further-reading.adoc similarity index 94% rename from src/reference/asciidoc/further-reading.adoc rename to src/reference/antora/modules/ROOT/pages/further-reading.adoc index 37c01eea7c..d199110deb 100644 --- a/src/reference/asciidoc/further-reading.adoc +++ b/src/reference/antora/modules/ROOT/pages/further-reading.adoc @@ -1,5 +1,6 @@ [[further-reading]] -=== Further Reading += Further Reading +:page-section-summary-toc: 1 For those who are not familiar with AMQP, the https://www.amqp.org/resources/download[specification] is actually quite readable. It is, of course, the authoritative source of information, and the Spring AMQP code should be easy to understand for anyone who is familiar with the spec. diff --git a/src/reference/asciidoc/preface.adoc b/src/reference/antora/modules/ROOT/pages/index.adoc similarity index 51% rename from src/reference/asciidoc/preface.adoc rename to src/reference/antora/modules/ROOT/pages/index.adoc index 5f07f478e1..d69ae83140 100644 --- a/src/reference/asciidoc/preface.adoc +++ b/src/reference/antora/modules/ROOT/pages/index.adoc @@ -1,3 +1,13 @@ +[[spring-amqp-reference]] += Spring AMQP +ifdef::backend-html5[] +:revnumber: '' +endif::[] +:numbered: +:icons: font +:hide-uri-scheme: +Mark Pollack; Mark Fisher; Oleg Zhurakousky; Dave Syer; Gary Russell; Gunnar Hillert; Artem Bilan; Stéphane Nicoll; Arnaud Cogoluègnes; Jay Bryant + [[preface]] The Spring AMQP project applies core Spring concepts to the development of AMQP-based messaging solutions. We provide a "`template`" as a high-level abstraction for sending and receiving messages. @@ -5,3 +15,7 @@ We also provide support for message-driven POJOs. These libraries facilitate management of AMQP resources while promoting the use of dependency injection and declarative configuration. In all of these cases, you can see similarities to the JMS support in the Spring Framework. For other project-related information, visit the Spring AMQP project https://projects.spring.io/spring-amqp/[homepage]. + +(C) 2010 - 2021 by VMware, Inc. + +Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically. \ No newline at end of file diff --git a/src/reference/antora/modules/ROOT/pages/integration-reference.adoc b/src/reference/antora/modules/ROOT/pages/integration-reference.adoc new file mode 100644 index 0000000000..f0d23fe5ea --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/integration-reference.adoc @@ -0,0 +1,4 @@ +[[spring-integration-reference]] += Spring Integration - Reference + +This part of the reference documentation provides a quick introduction to the AMQP support within the Spring Integration project. \ No newline at end of file diff --git a/src/reference/antora/modules/ROOT/pages/introduction/index.adoc b/src/reference/antora/modules/ROOT/pages/introduction/index.adoc new file mode 100644 index 0000000000..b88a71a602 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/introduction/index.adoc @@ -0,0 +1,5 @@ +[[introduction]] += Introduction + +This first part of the reference documentation is a high-level overview of Spring AMQP and the underlying concepts. +It includes some code snippets to get you up and running as quickly as possible. diff --git a/src/reference/asciidoc/quick-tour.adoc b/src/reference/antora/modules/ROOT/pages/introduction/quick-tour.adoc similarity index 93% rename from src/reference/asciidoc/quick-tour.adoc rename to src/reference/antora/modules/ROOT/pages/introduction/quick-tour.adoc index 75029ba7ea..21736104d4 100644 --- a/src/reference/asciidoc/quick-tour.adoc +++ b/src/reference/antora/modules/ROOT/pages/introduction/quick-tour.adoc @@ -1,7 +1,8 @@ [[quick-tour]] -=== Quick Tour for the impatient += Quick Tour for the impatient -==== Introduction +[[introduction]] +== Introduction This is the five-minute tour to get started with Spring AMQP. @@ -9,7 +10,6 @@ Prerequisites: Install and run the RabbitMQ broker (https://www.rabbitmq.com/dow Then grab the spring-rabbit JAR and all its dependencies - the easiest way to do so is to declare a dependency in your build tool. For example, for Maven, you can do something resembling the following: -==== [source,xml,subs="+attributes"] ---- @@ -18,19 +18,16 @@ For example, for Maven, you can do something resembling the following: {project-version} ---- -==== For Gradle, you can do something resembling the following: -==== [source,groovy,subs="+attributes"] ---- compile 'org.springframework.amqp:spring-rabbit:{project-version}' ---- -==== [[compatibility]] -===== Compatibility +=== Compatibility The minimum Spring Framework version dependency is 6.1.0. @@ -38,13 +35,13 @@ The minimum `amqp-client` Java client library version is 5.18.0. The minimum `stream-client` Java client library for stream queues is 0.12.0. -===== Very, Very Quick +[[very-very-quick]] +=== Very, Very Quick This section offers the fastest introduction. First, add the following `import` statements to make the examples later in this section work: -==== [source, java] ---- import org.springframework.amqp.core.AmqpAdmin; @@ -55,11 +52,9 @@ import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitAdmin; import org.springframework.amqp.rabbit.core.RabbitTemplate; ---- -==== The following example uses plain, imperative Java to send and receive a message: -==== [source,java] ---- ConnectionFactory connectionFactory = new CachingConnectionFactory(); @@ -69,7 +64,6 @@ AmqpTemplate template = new RabbitTemplate(connectionFactory); template.convertAndSend("myqueue", "foo"); String foo = (String) template.receiveAndConvert("myqueue"); ---- -==== Note that there is also a `ConnectionFactory` in the native Java Rabbit client. We use the Spring abstraction in the preceding code. @@ -77,11 +71,11 @@ It caches channels (and optionally connections) for reuse. We rely on the default exchange in the broker (since none is specified in the send), and the default binding of all queues to the default exchange by their name (thus, we can use the queue name as a routing key in the send). Those behaviors are defined in the AMQP specification. -===== With XML Configuration +[[with-xml-configuration]] +=== With XML Configuration The following example is the same as the preceding example but externalizes the resource configuration to XML: -==== [source,java] ---- ApplicationContext context = @@ -111,18 +105,17 @@ String foo = (String) template.receiveAndConvert("myqueue"); ---- -==== By default, the `` declaration automatically looks for beans of type `Queue`, `Exchange`, and `Binding` and declares them to the broker on behalf of the user. As a result, you need not use that bean explicitly in the simple Java driver. There are plenty of options to configure the properties of the components in the XML schema. You can use auto-complete features of your XML editor to explore them and look at their documentation. -===== With Java Configuration +[[with-java-configuration]] +=== With Java Configuration The following example repeats the same example as the preceding example but with the external configuration defined in Java: -==== [source,java] ---- ApplicationContext context = @@ -157,13 +150,12 @@ public class RabbitConfiguration { } } ---- -==== -===== With Spring Boot Auto Configuration and an Async POJO Listener +[[with-spring-boot-auto-configuration-and-an-async-pojo-listener]] +=== With Spring Boot Auto Configuration and an Async POJO Listener Spring Boot automatically configures the infrastructure beans, as the following example shows: -==== [source, java] ---- @SpringBootApplication @@ -190,4 +182,3 @@ public class Application { } ---- -==== diff --git a/src/reference/asciidoc/logging.adoc b/src/reference/antora/modules/ROOT/pages/logging.adoc similarity index 93% rename from src/reference/asciidoc/logging.adoc rename to src/reference/antora/modules/ROOT/pages/logging.adoc index fe07bf4d69..21b6855381 100644 --- a/src/reference/asciidoc/logging.adoc +++ b/src/reference/antora/modules/ROOT/pages/logging.adoc @@ -1,5 +1,5 @@ [[logging]] -=== Logging Subsystem AMQP Appenders += Logging Subsystem AMQP Appenders The framework provides logging appenders for some popular logging subsystems: @@ -8,7 +8,8 @@ The framework provides logging appenders for some popular logging subsystems: The appenders are configured by using the normal mechanisms for the logging subsystem, available properties are specified in the following sections. -==== Common properties +[[common-properties]] +== Common properties The following properties are available with all appenders: @@ -34,7 +35,7 @@ See `declareExchange`. | applicationId | -| Application ID -- added to the routing key if the pattern includes `%X{applicationId}`. +| Application ID -- added to the routing key if the pattern includes `+%X{applicationId}+`. | senderPoolSize | 2 @@ -72,12 +73,12 @@ Retries are delayed as follows: `N ^ log(N)`, where `N` is the retry number. | useSsl | false | Whether to use SSL for the RabbitMQ connection. -See <> +See xref:amqp/connections.adoc#rabbitconnectionfactorybean-configuring-ssl[`RabbitConnectionFactoryBean` and Configuring SSL] | verifyHostname | true | Enable server hostname verification for TLS connections. -See <> +See xref:amqp/connections.adoc#rabbitconnectionfactorybean-configuring-ssl[`RabbitConnectionFactoryBean` and Configuring SSL] | sslAlgorithm | null @@ -165,11 +166,11 @@ Please note, the `JsonLayout` adds MDC into the message by default. |=== -==== Log4j 2 Appender +[[log4j-2-appender]] +== Log4j 2 Appender The following example shows how to configure a Log4j 2 appender: -==== [source, xml] ---- @@ -185,7 +186,6 @@ The following example shows how to configure a Log4j 2 appender: ---- -==== [IMPORTANT] ==== @@ -198,11 +198,11 @@ One way to do that is to set the system property `-Dlog4j2.enable.threadlocals=f If you use asynchronous publishing with the `ReusableLogEventFactory`, events have a high likelihood of being corrupted due to cross-talk. ==== -==== Logback Appender +[[logback-appender]] +== Logback Appender The following example shows how to configure a logback appender: -==== [source, xml] ---- @@ -222,7 +222,6 @@ The following example shows how to configure a logback appender: false ---- -==== Starting with version 1.7.1, the Logback `AmqpAppender` provides an `includeCallerData` option, which is `false` by default. Extracting caller data can be rather expensive, because the log event has to create a throwable and inspect it to determine the calling location. @@ -232,7 +231,8 @@ You can configure the appender to include caller data by setting the `includeCal Starting with version 2.0.0, the Logback `AmqpAppender` supports https://logback.qos.ch/manual/encoders.html[Logback encoders] with the `encoder` option. The `encoder` and `layout` options are mutually exclusive. -==== Customizing the Messages +[[customizing-the-messages]] +== Customizing the Messages By default, AMQP appenders populate the following message properties: @@ -255,7 +255,6 @@ Each of the appenders can be subclassed, letting you modify the messages before The following example shows how to customize log messages: -==== [source, java] ---- public class MyEnhancedAppender extends AmqpAppender { @@ -268,12 +267,10 @@ public class MyEnhancedAppender extends AmqpAppender { } ---- -==== Starting with 2.2.4, the log4j2 `AmqpAppender` can be extended using `@PluginBuilderFactory` and extending also `AmqpAppender.Builder` -==== [source, java] ---- @Plugin(name = "MyEnhancedAppender", category = "Core", elementType = "appender", printObject = true) @@ -305,21 +302,21 @@ public class MyEnhancedAppender extends AmqpAppender { } ---- -==== -==== Customizing the Client Properties +[[customizing-the-client-properties]] +== Customizing the Client Properties You can add custom client properties by adding either string properties or more complex properties. -===== Simple String Properties +[[simple-string-properties]] +=== Simple String Properties Each appender supports adding client properties to the RabbitMQ connection. The following example shows how to add a custom client property for logback: -==== [source, xml] ---- @@ -328,10 +325,8 @@ The following example shows how to add a custom client property for logback: ... ---- -==== .log4j2 -==== [source, xml] ---- @@ -343,20 +338,19 @@ The following example shows how to add a custom client property for logback: ---- -==== The properties are a comma-delimited list of `key:value` pairs. Keys and values cannot contain commas or colons. These properties appear on the RabbitMQ Admin UI when the connection is viewed. -===== Advanced Technique for Logback +[[advanced-technique-for-logback]] +=== Advanced Technique for Logback You can subclass the Logback appender. Doing so lets you modify the client connection properties before the connection is established. The following example shows how to do so: -==== [source, java] ---- public class MyEnhancedAppender extends AmqpAppender { @@ -374,14 +368,14 @@ public class MyEnhancedAppender extends AmqpAppender { } ---- -==== Then you can add `thing2` to logback.xml. For String properties such as those shown in the preceding example, the previous technique can be used. Subclasses allow for adding richer properties (such as adding a `Map` or numeric property). -==== Providing a Custom Queue Implementation +[[providing-a-custom-queue-implementation]] +== Providing a Custom Queue Implementation The `AmqpAppenders` use a `BlockingQueue` to asynchronously publish logging events to RabbitMQ. By default, a `LinkedBlockingQueue` is used. @@ -389,7 +383,6 @@ However, you can supply any kind of custom `BlockingQueue` implementation. The following example shows how to do so for Logback: -==== [source, java] ---- public class MyEnhancedAppender extends AmqpAppender { @@ -401,11 +394,9 @@ public class MyEnhancedAppender extends AmqpAppender { } ---- -==== The Log4j 2 appender supports using a https://logging.apache.org/log4j/2.x/manual/appenders.html#BlockingQueueFactory[`BlockingQueueFactory`], as the following example shows: -==== [source, xml] ---- @@ -416,4 +407,3 @@ The Log4j 2 appender supports using a https://logging.apache.org/log4j/2.x/manua ---- -==== diff --git a/src/reference/antora/modules/ROOT/pages/reference.adoc b/src/reference/antora/modules/ROOT/pages/reference.adoc new file mode 100644 index 0000000000..247bbac91b --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/reference.adoc @@ -0,0 +1,9 @@ +[[reference]] += Reference + +This part of the reference documentation details the various components that comprise Spring AMQP. +The xref:amqp.adoc[main chapter] covers the core classes to develop an AMQP application. +This part also includes a chapter about the xref:sample-apps.adoc[sample applications]. + + + diff --git a/src/reference/antora/modules/ROOT/pages/resources.adoc b/src/reference/antora/modules/ROOT/pages/resources.adoc new file mode 100644 index 0000000000..f372038923 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/resources.adoc @@ -0,0 +1,6 @@ +[[resources]] += Other Resources + +In addition to this reference documentation, there exist a number of other resources that may help you learn about AMQP. + + diff --git a/src/reference/asciidoc/sample-apps.adoc b/src/reference/antora/modules/ROOT/pages/sample-apps.adoc similarity index 96% rename from src/reference/asciidoc/sample-apps.adoc rename to src/reference/antora/modules/ROOT/pages/sample-apps.adoc index b75dc60181..188a097b3c 100644 --- a/src/reference/asciidoc/sample-apps.adoc +++ b/src/reference/antora/modules/ROOT/pages/sample-apps.adoc @@ -1,5 +1,5 @@ [[sample-apps]] -=== Sample Applications += Sample Applications The https://github.com/SpringSource/spring-amqp-samples[Spring AMQP Samples] project includes two sample applications. The first is a simple "`Hello World`" example that demonstrates both synchronous and asynchronous message reception. @@ -9,13 +9,13 @@ In this chapter, we provide a quick walk-through of each sample so that you can The samples are both Maven-based, so you should be able to import them directly into any Maven-aware IDE (such as https://www.springsource.org/sts[SpringSource Tool Suite]). [[hello-world-sample]] -==== The "`Hello World`" Sample +== The "`Hello World`" Sample The "`Hello World`" sample demonstrates both synchronous and asynchronous message reception. You can import the `spring-rabbit-helloworld` sample into the IDE and then follow the discussion below. [[hello-world-sync]] -===== Synchronous Example +=== Synchronous Example Within the `src/main/java` directory, navigate to the `org.springframework.amqp.helloworld` package. Open the `HelloWorldConfiguration` class and notice that it contains the `@Configuration` annotation at the class level and notice some `@Bean` annotations at method-level. @@ -24,7 +24,6 @@ You can read more about that https://docs.spring.io/spring/docs/current/spring-f The following listing shows how the connection factory is created: -==== [source,java] ---- @Bean @@ -36,14 +35,12 @@ public CachingConnectionFactory connectionFactory() { return connectionFactory; } ---- -==== The configuration also contains an instance of `RabbitAdmin`, which, by default, looks for any beans of type exchange, queue, or binding and then declares them on the broker. In fact, the `helloWorldQueue` bean that is generated in `HelloWorldConfiguration` is an example because it is an instance of `Queue`. The following listing shows the `helloWorldQueue` bean definition: -==== [source,java] ---- @Bean @@ -51,7 +48,6 @@ public Queue helloWorldQueue() { return new Queue(this.helloWorldQueueName); } ---- -==== Looking back at the `rabbitTemplate` bean configuration, you can see that it has the name of `helloWorldQueue` set as its `queue` property (for receiving messages) and for its `routingKey` property (for sending messages). @@ -61,7 +57,6 @@ It contains a `main()` method where the Spring `ApplicationContext` is created. The following listing shows the `main` method: -==== [source,java] ---- public static void main(String[] args) { @@ -72,7 +67,6 @@ public static void main(String[] args) { System.out.println("Sent: Hello World"); } ---- -==== In the preceding example, the `AmqpTemplate` bean is retrieved and used for sending a `Message`. Since the client code should rely on interfaces whenever possible, the type is `AmqpTemplate` rather than `RabbitTemplate`. @@ -83,12 +77,11 @@ In this case, it uses the default `SimpleMessageConverter`, but a different impl Now open the `Consumer` class. It actually shares the same configuration base class, which means it shares the `rabbitTemplate` bean. That is why we configured that template with both a `routingKey` (for sending) and a `queue` (for receiving). -As we describe in <>, you could instead pass the 'routingKey' argument to the send method and the 'queue' argument to the receive method. +As we describe in xref:amqp/template.adoc[`AmqpTemplate`], you could instead pass the 'routingKey' argument to the send method and the 'queue' argument to the receive method. The `Consumer` code is basically a mirror image of the Producer, calling `receiveAndConvert()` rather than `convertAndSend()`. The following listing shows the main method for the `Consumer`: -==== [source,java] ---- public static void main(String[] args) { @@ -98,14 +91,13 @@ public static void main(String[] args) { System.out.println("Received: " + amqpTemplate.receiveAndConvert()); } ---- -==== If you run the `Producer` and then run the `Consumer`, you should see `Received: Hello World` in the console output. [[hello-world-async]] -===== Asynchronous Example +=== Asynchronous Example -<> walked through the synchronous Hello World sample. +xref:sample-apps.adoc#hello-world-sync[Synchronous Example] walked through the synchronous Hello World sample. This section describes a slightly more advanced but significantly more powerful option. With a few modifications, the Hello World sample can provide an example of asynchronous reception, also known as message-driven POJOs. In fact, there is a sub-package that provides exactly that: `org.springframework.amqp.samples.helloworld.async`. @@ -120,7 +112,6 @@ That is why we only need to provide the routing key here. The following listing shows the `rabbitTemplate` definition: -==== [source,java] ---- public RabbitTemplate rabbitTemplate() { @@ -129,7 +120,6 @@ public RabbitTemplate rabbitTemplate() { return template; } ---- -==== Since this sample demonstrates asynchronous message reception, the producing side is designed to continuously send messages (if it were a message-per-execution model like the synchronous version, it would not be quite so obvious that it is, in fact, a message-driven consumer). The component responsible for continuously sending messages is defined as an inner class within the `ProducerConfiguration`. @@ -137,7 +127,6 @@ It is configured to run every three seconds. The following listing shows the component: -==== [source,java] ---- static class ScheduledProducer { @@ -153,7 +142,6 @@ static class ScheduledProducer { } } ---- -==== You do not need to understand all of the details, since the real focus should be on the receiving side (which we cover next). However, if you are not yet familiar with Spring task scheduling support, you can learn more https://docs.spring.io/spring/docs/current/spring-framework-reference/html/scheduling.html#scheduling-annotation-support[here]. @@ -163,7 +151,6 @@ Now we can turn to the receiving side. To emphasize the message-driven POJO behavior, we start with the component that react to the messages. The class is called `HelloWorldHandler` and is shown in the following listing: -==== [source,java] ---- public class HelloWorldHandler { @@ -174,7 +161,6 @@ public class HelloWorldHandler { } ---- -==== That class is a POJO. It does not extend any base class, it does not implement any interfaces, and it does not even contain any imports. @@ -185,7 +171,6 @@ You can see the POJO wrapped in the adapter there. The following listing shows how the `listenerContainer` is defined: -==== [source,java] ---- @Bean @@ -197,16 +182,16 @@ public SimpleMessageListenerContainer listenerContainer() { return container; } ---- -==== The `SimpleMessageListenerContainer` is a Spring lifecycle component and, by default, starts automatically. If you look in the `Consumer` class, you can see that its `main()` method consists of nothing more than a one-line bootstrap to create the `ApplicationContext`. The Producer's `main()` method is also a one-line bootstrap, since the component whose method is annotated with `@Scheduled` also starts automatically. You can start the `Producer` and `Consumer` in any order, and you should see messages being sent and received every three seconds. -==== Stock Trading +[[stock-trading]] +== Stock Trading -The Stock Trading sample demonstrates more advanced messaging scenarios than <>. +The Stock Trading sample demonstrates more advanced messaging scenarios than xref:sample-apps.adoc#hello-world-sample[the Hello World sample]. However, the configuration is very similar, if a bit more involved. Since we walked through the Hello World configuration in detail, here, we focus on what makes this sample different. There is a server that pushes market data (stock quotations) to a topic exchange. @@ -225,21 +210,18 @@ First, it configures the market data exchange on the `RabbitTemplate` so that it It does this within an abstract callback method defined in the base configuration class. The following listing shows that method: -==== [source,java] ---- public void configureRabbitTemplate(RabbitTemplate rabbitTemplate) { rabbitTemplate.setExchange(MARKET_DATA_EXCHANGE_NAME); } ---- -==== Second, the stock request queue is declared. It does not require any explicit bindings in this case, because it is bound to the default no-name exchange with its own name as the routing key. As mentioned earlier, the AMQP specification defines that behavior. The following listing shows the definition of the `stockRequestQueue` bean: -==== [source,java] ---- @Bean @@ -247,7 +229,6 @@ public Queue stockRequestQueue() { return new Queue(STOCK_REQUEST_QUEUE_NAME); } ---- -==== Now that you have seen the configuration of the server's AMQP resources, navigate to the `org.springframework.amqp.rabbit.stocks` package under the `src/test/java` directory. There, you can see the actual `Server` class that provides a `main()` method. @@ -263,20 +244,17 @@ Notice that it is not itself coupled to the framework or any of the AMQP concept It accepts a `TradeRequest` and returns a `TradeResponse`. The following listing shows the definition of the `handleMessage` method: -==== [source,java] ---- public TradeResponse handleMessage(TradeRequest tradeRequest) { ... } ---- -==== Now that we have seen the most important configuration and code for the server, we can turn to the client. The best starting point is probably `RabbitClientConfiguration`, in the `org.springframework.amqp.rabbit.stocks.config.client` package. Notice that it declares two queues without providing explicit names. The following listing shows the bean definitions for the two queues: -==== [source,java] ---- @Bean @@ -289,7 +267,6 @@ public Queue traderJoeQueue() { return amqpAdmin().declareQueue(); } ---- -==== Those are private queues, and unique names are generated automatically. The first generated queue is used by the client to bind to the market data exchange that has been exposed by the server. @@ -299,7 +276,6 @@ Since the market data exchange is a topic exchange, the binding can be expressed The `RabbitClientConfiguration` does so with a `Binding` object, and that object is generated with the `BindingBuilder` fluent API. The following listing shows the `Binding`: -==== [source,java] ---- @Value("${stocks.quote.pattern}") @@ -311,7 +287,6 @@ public Binding marketDataBinding() { marketDataQueue()).to(marketDataExchange()).with(marketDataRoutingKey); } ---- -==== Notice that the actual value has been externalized in a properties file (`client.properties` under `src/main/resources`), and that we use Spring's `@Value` annotation to inject that value. This is generally a good idea. @@ -331,7 +306,6 @@ The corresponding code on the `Client` side is `RabbitStockServiceGateway` in th It delegates to the `RabbitTemplate` in order to send messages. The following listing shows the `send` method: -==== [source,java] ---- public void send(TradeRequest tradeRequest) { @@ -350,13 +324,11 @@ public void send(TradeRequest tradeRequest) { }); } ---- -==== Notice that, prior to sending the message, it sets the `replyTo` address. It provides the queue that was generated by the `traderJoeQueue` bean definition (shown earlier). The following listing shows the `@Bean` definition for the `StockServiceGateway` class itself: -==== [source,java] ---- @Bean @@ -367,14 +339,13 @@ public StockServiceGateway stockServiceGateway() { return gateway; } ---- -==== If you are no longer running the server and client, start them now. Try sending a request with the format of '100 TCKR'. After a brief artificial delay that simulates "`processing`" of the request, you should see a confirmation message appear on the client. [[spring-rabbit-json]] -==== Receiving JSON from Non-Spring Applications +== Receiving JSON from Non-Spring Applications Spring applications, when sending JSON, set the `__TypeId__` header to the fully qualified class name to assist the receiving application in converting the JSON back to a Java object. diff --git a/src/reference/asciidoc/si-amqp.adoc b/src/reference/antora/modules/ROOT/pages/si-amqp.adoc similarity index 92% rename from src/reference/asciidoc/si-amqp.adoc rename to src/reference/antora/modules/ROOT/pages/si-amqp.adoc index c52c40c6aa..02d5fbf7b6 100644 --- a/src/reference/asciidoc/si-amqp.adoc +++ b/src/reference/antora/modules/ROOT/pages/si-amqp.adoc @@ -1,10 +1,10 @@ [[spring-integration-amqp]] -=== Spring Integration AMQP Support += Spring Integration AMQP Support This brief chapter covers the relationship between the Spring Integration and the Spring AMQP projects. [[spring-integration-amqp-introduction]] -==== Introduction +== Introduction The https://www.springsource.org/spring-integration[Spring Integration] project includes AMQP Channel Adapters and Gateways that build upon the Spring AMQP project. Those adapters are developed and released in the Spring Integration project. @@ -15,27 +15,26 @@ Since the AMQP adapters are part of the Spring Integration release, the document We provide a quick overview of the main features here. See the https://docs.spring.io/spring-integration/reference/htmlsingle/[Spring Integration Reference Guide] for much more detail. -==== Inbound Channel Adapter +[[inbound-channel-adapter]] +== Inbound Channel Adapter To receive AMQP Messages from a queue, you can configure an ``. The following example shows how to configure an inbound channel adapter: -==== [source,xml] ---- ---- -==== -==== Outbound Channel Adapter +[[outbound-channel-adapter]] +== Outbound Channel Adapter To send AMQP Messages to an exchange, you can configure an ``. You can optionally provide a 'routing-key' in addition to the exchange name. The following example shows how to define an outbound channel adapter: -==== [source,xml] ---- ---- -==== -==== Inbound Gateway +[[inbound-gateway]] +== Inbound Gateway To receive an AMQP Message from a queue and respond to its reply-to address, you can configure an ``. The following example shows how to define an inbound gateway: -==== [source,xml] ---- ---- -==== -==== Outbound Gateway +[[outbound-gateway]] +== Outbound Gateway To send AMQP Messages to an exchange and receive back a response from a remote client, you can configure an ``. You can optionally provide a 'routing-key' in addition to the exchange name. The following example shows how to define an outbound gateway: -==== [source,xml] ---- ---- -==== diff --git a/src/reference/asciidoc/stream.adoc b/src/reference/antora/modules/ROOT/pages/stream.adoc similarity index 92% rename from src/reference/asciidoc/stream.adoc rename to src/reference/antora/modules/ROOT/pages/stream.adoc index a53c2e4889..52cbc6dc18 100644 --- a/src/reference/asciidoc/stream.adoc +++ b/src/reference/antora/modules/ROOT/pages/stream.adoc @@ -1,5 +1,5 @@ [[stream-support]] -=== Using the RabbitMQ Stream Plugin += Using the RabbitMQ Stream Plugin Version 2.4 introduces initial support for the https://github.com/rabbitmq/rabbitmq-stream-java-client[RabbitMQ Stream Plugin Java Client] for the https://rabbitmq.com/stream.html[RabbitMQ Stream Plugin]. @@ -9,7 +9,6 @@ Version 2.4 introduces initial support for the https://github.com/rabbitmq/rabbi Add the `spring-rabbit-stream` dependency to your project: .maven -==== [source,xml,subs="+attributes"] ---- @@ -18,35 +17,29 @@ Add the `spring-rabbit-stream` dependency to your project: {project-version} ---- -==== .gradle -==== [source,groovy,subs="+attributes"] ---- compile 'org.springframework.amqp:spring-rabbit-stream:{project-version}' ---- -==== You can provision the queues as normal, using a `RabbitAdmin` bean, using the `QueueBuilder.stream()` method to designate the queue type. For example: -==== [source, java] ---- @Bean Queue stream() { return QueueBuilder.durable("stream.queue1") .stream() - .build(); + .build(); } ---- -==== However, this will only work if you are also using non-stream components (such as the `SimpleMessageListenerContainer` or `DirectMessageListenerContainer`) because the admin is triggered to declare the defined beans when an AMQP connection is opened. If your application only uses stream components, or you wish to use advanced stream configuration features, you should configure a `StreamAdmin` instead: -==== [source, java] ---- @Bean @@ -57,16 +50,15 @@ StreamAdmin streamAdmin(Environment env) { }); } ---- -==== Refer to the RabbitMQ documentation for more information about the `StreamCreator`. -==== Sending Messages +[[sending-messages]] +== Sending Messages The `RabbitStreamTemplate` provides a subset of the `RabbitTemplate` (AMQP) functionality. .RabbitStreamOperations -==== [source, java] ---- public interface RabbitStreamOperations extends AutoCloseable { @@ -90,12 +82,10 @@ public interface RabbitStreamOperations extends AutoCloseable { } ---- -==== The `RabbitStreamTemplate` implementation has the following constructor and properties: .RabbitStreamTemplate -==== [source, java] ---- public RabbitStreamTemplate(Environment environment, String streamName) { @@ -110,7 +100,6 @@ public void setStreamConverter(StreamMessageConverter streamConverter) { public synchronized void setProducerCustomizer(ProducerCustomizer producerCustomizer) { } ---- -==== The `MessageConverter` is used in the `convertAndSend` methods to convert the object to a Spring AMQP `Message`. @@ -124,7 +113,8 @@ Refer to the https://rabbitmq.github.io/rabbitmq-stream-java-client/stable/htmls IMPORTANT: Starting with version 3.0, the method return types are `CompletableFuture` instead of `ListenableFuture`. -==== Receiving Messages +[[receiving-messages]] +== Receiving Messages Asynchronous message reception is provided by the `StreamListenerContainer` (and the `StreamRabbitListenerContainerFactory` when using `@RabbitListener`). @@ -132,7 +122,6 @@ The listener container requires an `Environment` as well as a single stream name You can either receive Spring AMQP `Message` s using the classic `MessageListener`, or you can receive native stream `Message` s using a new interface: -==== [source, java] ---- public interface StreamMessageListener extends MessageListener { @@ -141,9 +130,8 @@ public interface StreamMessageListener extends MessageListener { } ---- -==== -See <> for information about supported properties. +See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration] for information about supported properties. Similar the template, the container has a `ConsumerCustomizer` property. @@ -153,9 +141,8 @@ When using `@RabbitListener`, configure a `StreamRabbitListenerContainerFactory` In addition, `queues` can only contain one stream name. [[stream-examples]] -==== Examples +== Examples -==== [source, java] ---- @Bean @@ -197,22 +184,20 @@ void nativeMsg(Message in, Context context) { Queue stream() { return QueueBuilder.durable("test.stream.queue1") .stream() - .build(); + .build(); } @Bean Queue stream() { return QueueBuilder.durable("test.stream.queue2") .stream() - .build(); + .build(); } ---- -==== Version 2.4.5 added the `adviceChain` property to the `StreamListenerContainer` (and its factory). A new factory bean is also provided to create a stateless retry interceptor with an optional `StreamMessageRecoverer` for use when consuming raw stream messages. -==== [source, java] ---- @Bean @@ -226,19 +211,19 @@ public StreamRetryOperationsInterceptorFactoryBean sfb(RetryTemplate retryTempla return rfb; } ---- -==== IMPORTANT: Stateful retry is not supported with this container. -==== Super Streams +[[super-streams]] +== Super Streams A Super Stream is an abstract concept for a partitioned stream, implemented by binding a number of stream queues to an exchange having an argument `x-super-stream: true`. -===== Provisioning +[[provisioning]] +=== Provisioning For convenience, a super stream can be provisioned by defining a single bean of type `SuperStream`. -==== [source, java] ---- @Bean @@ -246,13 +231,11 @@ SuperStream superStream() { return new SuperStream("my.super.stream", 3); } ---- -==== The `RabbitAdmin` detects this bean and will declare the exchange (`my.super.stream`) and 3 queues (partitions) - `my.super-stream-n` where `n` is `0`, `1`, `2`, bound with routing keys equal to `n`. If you also wish to publish over AMQP to the exchange, you can provide custom routing keys: -==== [source, java] ---- @Bean @@ -262,15 +245,14 @@ SuperStream superStream() { .collect(Collectors.toList())); } ---- -==== The number of keys must equal the number of partitions. -===== Producing to a SuperStream +[[producing-to-a-superstream]] +=== Producing to a SuperStream You must add a `superStreamRoutingFunction` to the `RabbitStreamTemplate`: -==== [source, java] ---- @Bean @@ -282,16 +264,14 @@ RabbitStreamTemplate streamTemplate(Environment env) { return template; } ---- -==== You can also publish over AMQP, using the `RabbitTemplate`. [[super-stream-consumer]] -===== Consuming Super Streams with Single Active Consumers +=== Consuming Super Streams with Single Active Consumers Invoke the `superStream` method on the listener container to enable a single active consumer on a super stream. -==== [source, java] ---- @Bean @@ -305,18 +285,17 @@ StreamListenerContainer container(Environment env, String name) { return container; } ---- -==== IMPORTANT: At this time, when the concurrency is greater than 1, the actual concurrency is further controlled by the `Environment`; to achieve full concurrency, set the environment's `maxConsumersByConnection` to 1. See https://rabbitmq.github.io/rabbitmq-stream-java-client/snapshot/htmlsingle/#configuring-the-environment[Configuring the Environment]. [[stream-micrometer-observation]] -==== Micrometer Observation +== Micrometer Observation Using Micrometer for observation is now supported, since version 3.0.5, for the `RabbitStreamTemplate` and the stream listener container. The container now also supports Micrometer timers (when observation is not enabled). -Set `observationEnabled` on each component to enable observation; this will disable <> because the timers will now be managed with each observation. +Set `observationEnabled` on each component to enable observation; this will disable xref:amqp/receiving-messages/micrometer.adoc[Micrometer Timers] because the timers will now be managed with each observation. When using annotated listeners, set `observationEnabled` on the container factory. Refer to https://micrometer.io/docs/tracing[Micrometer Tracing] for more information. @@ -327,4 +306,4 @@ The default implementations add the `name` tag for template observations and `li You can either subclass `DefaultRabbitStreamTemplateObservationConvention` or `DefaultStreamRabbitListenerObservationConvention` or provide completely new implementations. -See <> for more details. +See xref:appendix/micrometer.adoc[Micrometer Observation Documentation] for more details. diff --git a/src/reference/asciidoc/testing.adoc b/src/reference/antora/modules/ROOT/pages/testing.adoc similarity index 94% rename from src/reference/asciidoc/testing.adoc rename to src/reference/antora/modules/ROOT/pages/testing.adoc index feec38c865..96e84abcd0 100644 --- a/src/reference/asciidoc/testing.adoc +++ b/src/reference/antora/modules/ROOT/pages/testing.adoc @@ -1,5 +1,5 @@ [[testing]] -=== Testing Support += Testing Support Writing integration for asynchronous applications is necessarily more complex than testing simpler applications. This is made more complex when abstractions such as the `@RabbitListener` annotations come into the picture. @@ -14,7 +14,7 @@ It is anticipated that this project will expand over time, but we need community Please use https://jira.spring.io/browse/AMQP[JIRA] or https://github.com/spring-projects/spring-amqp/issues[GitHub Issues] to provide such feedback. [[spring-rabbit-test]] -==== @SpringRabbitTest +== @SpringRabbitTest Use this annotation to add infrastructure beans to the Spring test `ApplicationContext`. This is not necessary when using, for example `@SpringBootTest` since Spring Boot's auto configuration will add the beans. @@ -29,7 +29,6 @@ Beans that are registered are: In addition, the beans associated with `@EnableRabbit` (to support `@RabbitListener`) are added. .Junit5 example -==== [source, java] ---- @SpringJunitConfig @@ -59,19 +58,17 @@ public class MyRabbitTests { } ---- -==== With JUnit4, replace `@SpringJunitConfig` with `@RunWith(SpringRunnner.class)`. [[mockito-answer]] -==== Mockito `Answer` Implementations +== Mockito `Answer` Implementations There are currently two `Answer` implementations to help with testing. The first, `LatchCountDownAndCallRealMethodAnswer`, provides an `Answer` that returns `null` and counts down a latch. The following example shows how to use `LatchCountDownAndCallRealMethodAnswer`: -==== [source, java] ---- LatchCountDownAndCallRealMethodAnswer answer = this.harness.getLatchAnswerFor("myListener", 2); @@ -82,14 +79,12 @@ doAnswer(answer) assertThat(answer.await(10)).isTrue(); ---- -==== The second, `LambdaAnswer` provides a mechanism to optionally call the real method and provides an opportunity to return a custom result, based on the `InvocationOnMock` and the result (if any). Consider the following POJO: -==== [source, java] ---- public class Thing { @@ -100,11 +95,9 @@ public class Thing { } ---- -==== The following class tests the `Thing` POJO: -==== [source, java] ---- Thing thing = spy(new Thing()); @@ -121,15 +114,14 @@ doAnswer(new LambdaAnswer(false, (i, r) -> "" + i.getArguments()[0] + i.getArguments()[0])).when(thing).thing(anyString()); assertEquals("thingthing", thing.thing("thing")); ---- -==== Starting with version 2.2.3, the answers capture any exceptions thrown by the method under test. Use `answer.getExceptions()` to get a reference to them. -When used in conjunction with the <> use `harness.getLambdaAnswerFor("listenerId", true, ...)` to get a properly constructed answer for the listener. +When used in conjunction with the xref:testing.adoc#test-harness[`@RabbitListenerTest` and `RabbitListenerTestHarness`] use `harness.getLambdaAnswerFor("listenerId", true, ...)` to get a properly constructed answer for the listener. [[test-harness]] -==== `@RabbitListenerTest` and `RabbitListenerTestHarness` +== `@RabbitListenerTest` and `RabbitListenerTestHarness` Annotating one of your `@Configuration` classes with `@RabbitListenerTest` causes the framework to replace the standard `RabbitListenerAnnotationBeanPostProcessor` with a subclass called `RabbitListenerTestHarness` (it also enables @@ -149,7 +141,6 @@ Consider some examples. The following example uses spy: -==== [source, java] ---- @Configuration @@ -224,11 +215,9 @@ We use one of the link:#mockito-answer[Answer] implementations to help with t IMPORTANT: Due to the way the listener is spied, it is important to use `harness.getLatchAnswerFor()` to get a properly configured answer for the spy. <4> Configure the spy to invoke the `Answer`. -==== The following example uses the capture advice: -==== [source, java] ---- @Configuration @@ -311,13 +300,12 @@ for the result. to suspend the test thread. <5> When the listener throws an exception, it is available in the `throwable` property of the invocation data. -==== IMPORTANT: When using custom `Answer` s with the harness, in order to operate properly, such answers should subclass `ForwardsInvocation` and get the actual listener (not the spy) from the harness (`getDelegate("myListener")`) and call `super.answer(invocation)`. -See the provided <> source code for examples. +See the provided xref:testing.adoc#mockito-answer[Mockito `Answer` Implementations] source code for examples. [[test-template]] -==== Using `TestRabbitTemplate` +== Using `TestRabbitTemplate` The `TestRabbitTemplate` is provided to perform some basic integration testing without the need for a broker. When you add it as a `@Bean` in your test case, it discovers all the listener containers in the context, whether declared as `@Bean` or `` or using the `@RabbitListener` annotation. @@ -327,7 +315,6 @@ Request-reply messaging (`sendAndReceive` methods) is supported for listeners th The following test case uses the template: -==== [source, java] ---- @RunWith(SpringRunner.class) @@ -435,16 +422,16 @@ public class TestRabbitTemplateTests { } ---- -==== [[junit-rules]] -==== JUnit4 `@Rules` +== JUnit4 `@Rules` Spring AMQP version 1.7 and later provide an additional jar called `spring-rabbit-junit`. This jar contains a couple of utility `@Rule` instances for use when running JUnit4 tests. -See <> for JUnit5 testing. +See xref:testing.adoc#junit5-conditions[JUnit5 Conditions] for JUnit5 testing. -===== Using `BrokerRunning` +[[using-brokerrunning]] +=== Using `BrokerRunning` `BrokerRunning` provides a mechanism to let tests succeed when a broker is not running (on `localhost`, by default). @@ -452,7 +439,6 @@ It also has utility methods to initialize and empty queues and delete queues and The following example shows its usage: -==== [source, java] ---- @@ -464,12 +450,11 @@ public static void tearDown() { brokerRunning.removeTestQueues("some.other.queue.too") // removes foo, bar as well } ---- -==== There are several `isRunning...` static methods, such as `isBrokerAndManagementRunning()`, which verifies the broker has the management plugin enabled. [[brokerRunning-configure]] -====== Configuring the Rule +==== Configuring the Rule There are times when you want tests to fail if there is no broker, such as a nightly CI build. To disable the rule at runtime, set an environment variable called `RABBITMQ_SERVER_REQUIRED` to `true`. @@ -478,7 +463,6 @@ You can override the broker properties, such as hostname with either setters or The following example shows how to override properties with setters: -==== [source, java] ---- @@ -494,11 +478,9 @@ public static void tearDown() { brokerRunning.removeTestQueues("some.other.queue.too") // removes foo, bar as well } ---- -==== You can also override properties by setting the following environment variables: -==== [source, java] ---- public static final String BROKER_ADMIN_URI = "RABBITMQ_TEST_ADMIN_URI"; @@ -509,7 +491,6 @@ public static final String BROKER_PW = "RABBITMQ_TEST_PASSWORD"; public static final String BROKER_ADMIN_USER = "RABBITMQ_TEST_ADMIN_USER"; public static final String BROKER_ADMIN_PW = "RABBITMQ_TEST_ADMIN_PASSWORD"; ---- -==== These environment variables override the default settings (`localhost:5672` for amqp and `http://localhost:15672/api/` for the management REST API). @@ -525,7 +506,6 @@ Invoke `clearEnvironmentVariableOverrides()` to reset the rule to use defaults ( In your test cases, you can use the `brokerRunning` when creating the connection factory; `getConnectionFactory()` returns the rule's RabbitMQ `ConnectionFactory`. The following example shows how to do so: -==== [source, java] ---- @Bean @@ -533,31 +513,30 @@ public CachingConnectionFactory rabbitConnectionFactory() { return new CachingConnectionFactory(brokerRunning.getConnectionFactory()); } ---- -==== -===== Using `LongRunningIntegrationTest` +[[using-longrunningintegrationtest]] +=== Using `LongRunningIntegrationTest` `LongRunningIntegrationTest` is a rule that disables long running tests. You might want to use this on a developer system but ensure that the rule is disabled on, for example, nightly CI builds. The following example shows its usage: -==== [source, java] ---- @Rule public LongRunningIntegrationTest longTests = new LongRunningIntegrationTest(); ---- -==== To disable the rule at runtime, set an environment variable called `RUN_LONG_INTEGRATION_TESTS` to `true`. [[junit5-conditions]] -==== JUnit5 Conditions +== JUnit5 Conditions Version 2.0.2 introduced support for JUnit5. -===== Using the `@RabbitAvailable` Annotation +[[using-the-rabbitavailable-annotation]] +=== Using the `@RabbitAvailable` Annotation This class-level annotation is similar to the `BrokerRunning` `@Rule` discussed in <>. It is processed by the `RabbitAvailableCondition`. @@ -569,8 +548,8 @@ The annotation has three properties: * `purgeAfterEach`: (Since version 2.2) when `true` (default), the `queues` will be purged between tests. It is used to check whether the broker is available and skip the tests if not. -As discussed in <>, the environment variable called `RABBITMQ_SERVER_REQUIRED`, if `true`, causes the tests to fail fast if there is no broker. -You can configure the condition by using environment variables as discussed in <>. +As discussed in xref:testing.adoc#brokerRunning-configure[Configuring the Rule], the environment variable called `RABBITMQ_SERVER_REQUIRED`, if `true`, causes the tests to fail fast if there is no broker. +You can configure the condition by using environment variables as discussed in xref:testing.adoc#brokerRunning-configure[Configuring the Rule]. In addition, the `RabbitAvailableCondition` supports argument resolution for parameterized test constructors and methods. Two argument types are supported: @@ -580,7 +559,6 @@ Two argument types are supported: The following example shows both: -==== [source, java] ---- @RabbitAvailable(queues = "rabbitAvailableTests.queue") @@ -605,13 +583,11 @@ public class RabbitAvailableCTORInjectionTests { } ---- -==== The preceding test is in the framework itself and verifies the argument injection and that the condition created the queue properly. A practical user test might be as follows: -==== [source, java] ---- @RabbitAvailable(queues = "rabbitAvailableTests.queue") @@ -631,7 +607,6 @@ public class RabbitAvailableCTORInjectionTests { } } ---- -==== When you use a Spring annotation application context within a test class, you can get a reference to the condition's connection factory through a static method called `RabbitAvailableCondition.getBrokerRunning()`. @@ -640,7 +615,6 @@ The new class has the same API as `BrokerRunning`. The following test comes from the framework and demonstrates the usage: -==== [source, java] ---- @RabbitAvailable(queues = { @@ -702,14 +676,13 @@ public class RabbitTemplateMPPIntegrationTests { } ---- -==== -===== Using the `@LongRunning` Annotation +[[using-the-longrunning-annotation]] +=== Using the `@LongRunning` Annotation Similar to the `LongRunningIntegrationTest` JUnit4 `@Rule`, this annotation causes tests to be skipped unless an environment variable (or system property) is set to `true`. The following example shows how to use it: -==== [source, java] ---- @RabbitAvailable(queues = SimpleMessageListenerContainerLongTests.QUEUE) @@ -722,6 +695,5 @@ public class SimpleMessageListenerContainerLongTests { } ---- -==== By default, the variable is `RUN_LONG_INTEGRATION_TESTS`, but you can specify the variable name in the annotation's `value` attribute. diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/antora/modules/ROOT/pages/whats-new.adoc similarity index 67% rename from src/reference/asciidoc/whats-new.adoc rename to src/reference/antora/modules/ROOT/pages/whats-new.adoc index 23218ec70b..ff9eb62ad4 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/antora/modules/ROOT/pages/whats-new.adoc @@ -1,17 +1,20 @@ [[whats-new]] -== What's New += What's New +:page-section-summary-toc: 1 -=== Changes in 3.1 Since 3.0 +[[changes-in-3-1-since-3-0]] +== Changes in 3.1 Since 3.0 -==== Java 17, Spring Framework 6.1 +[[java-17-spring-framework-6-1]] +=== Java 17, Spring Framework 6.1 This version requires Spring Framework 6.1 and Java 17. [[x31-exc]] -==== Exclusive Consumer Logging +=== Exclusive Consumer Logging Log messages reporting access refusal due to exclusive consumers are now logged at DEBUG level by default. It remains possible to configure your own logging behavior by setting the `exclusiveConsumerExceptionLogger` and `closeExceptionLogger` properties on the listener container and connection factory respectively. In addition, the `SimpleMessageListenerContainer` consumer restart after such an exception is now logged at DEBUG level by default (previously INFO). A new method `logRestart()` has been added to the `ConditionalExceptionLogger` to allow this to be changed. -See <> and <> for more information. +See xref:amqp/receiving-messages/consumer-events.adoc[Consumer Events] and <> for more information. diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc deleted file mode 100644 index 879cf2a0a9..0000000000 --- a/src/reference/asciidoc/amqp.adoc +++ /dev/null @@ -1,7059 +0,0 @@ -[[amqp]] -=== Using Spring AMQP - -This chapter explores the interfaces and classes that are the essential components for developing applications with Spring AMQP. - -==== AMQP Abstractions - -Spring AMQP consists of two modules (each represented by a JAR in the distribution): `spring-amqp` and `spring-rabbit`. -The 'spring-amqp' module contains the `org.springframework.amqp.core` package. -Within that package, you can find the classes that represent the core AMQP "`model`". -Our intention is to provide generic abstractions that do not rely on any particular AMQP broker implementation or client library. -End user code can be more portable across vendor implementations as it can be developed against the abstraction layer only. -These abstractions are then implemented by broker-specific modules, such as 'spring-rabbit'. -There is currently only a RabbitMQ implementation. -However, the abstractions have been validated in .NET using Apache Qpid in addition to RabbitMQ. -Since AMQP operates at the protocol level, in principle, you can use the RabbitMQ client with any broker that supports the same protocol version, but we do not test any other brokers at present. - -This overview assumes that you are already familiar with the basics of the AMQP specification. -If not, have a look at the resources listed in <> - -===== `Message` - -The 0-9-1 AMQP specification does not define a `Message` class or interface. -Instead, when performing an operation such as `basicPublish()`, the content is passed as a byte-array argument and additional properties are passed in as separate arguments. -Spring AMQP defines a `Message` class as part of a more general AMQP domain model representation. -The purpose of the `Message` class is to encapsulate the body and properties within a single instance so that the API can, in turn, be simpler. -The following example shows the `Message` class definition: - -==== -[source,java] ----- -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; - } -} ----- -==== - -The `MessageProperties` interface defines several common properties, such as 'messageId', 'timestamp', 'contentType', and several more. -You can also extend those properties with user-defined 'headers' by calling the `setHeader(String key, Object value)` method. - -IMPORTANT: Starting with versions `1.5.7`, `1.6.11`, `1.7.4`, and `2.0.0`, if a message body is a serialized `Serializable` java object, it is no longer deserialized (by default) when performing `toString()` operations (such as in log messages). -This is to prevent unsafe deserialization. -By default, only `java.util` and `java.lang` classes are deserialized. -To revert to the previous behavior, you can add allowable class/package patterns by invoking `Message.addAllowedListPatterns(...)`. -A simple `*` wildcard is supported, for example `com.something.*, *.MyClass`. -Bodies that cannot be deserialized are represented by `byte[]` in log messages. - -===== Exchange - -The `Exchange` interface represents an AMQP Exchange, which is what a Message Producer sends to. -Each Exchange within a virtual host of a broker has a unique name as well as a few other properties. -The following example shows the `Exchange` interface: - -[source,java] ----- -public interface Exchange { - - String getName(); - - String getExchangeType(); - - boolean isDurable(); - - boolean isAutoDelete(); - - Map getArguments(); - -} ----- - -As you can see, an `Exchange` also has a 'type' represented by constants defined in `ExchangeTypes`. -The basic types are: `direct`, `topic`, `fanout`, and `headers`. -In the core package, you can find implementations of the `Exchange` interface for each of those types. -The behavior varies across these `Exchange` types in terms of how they handle bindings to queues. -For example, a `Direct` exchange lets a queue be bound by a fixed routing key (often the queue's name). -A `Topic` exchange supports bindings with routing patterns that may include the '*' and '#' wildcards for 'exactly-one' and 'zero-or-more', respectively. -The `Fanout` exchange publishes to all queues that are bound to it without taking any routing key into consideration. -For much more information about these and the other Exchange types, see <>. - -NOTE: The AMQP specification also requires that any broker provide a "`default`" direct exchange that has no name. -All queues that are declared are bound to that default `Exchange` with their names as routing keys. -You can learn more about the default Exchange's usage within Spring AMQP in <>. - -===== Queue - -The `Queue` class represents the component from which a message consumer receives messages. -Like the various `Exchange` classes, our implementation is intended to be an abstract representation of this core AMQP type. -The following listing shows the `Queue` class: - -==== -[source,java] ----- -public class Queue { - - private final String name; - - private volatile boolean durable; - - private volatile boolean exclusive; - - private volatile boolean autoDelete; - - private volatile Map 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 - -} ----- -==== - -Notice that the constructor takes the queue name. -Depending on the implementation, the admin template may provide methods for generating a uniquely named queue. -Such queues can be useful as a "`reply-to`" address or in other *temporary* situations. -For that reason, the 'exclusive' and 'autoDelete' properties of an auto-generated queue would both be set to 'true'. - -NOTE: See the section on queues in <> for information about declaring queues by using namespace support, including queue arguments. - -===== Binding - -Given that a producer sends to an exchange and a consumer receives from a queue, the bindings that connect queues to exchanges are critical for connecting those producers and consumers via messaging. -In Spring AMQP, we define a `Binding` class to represent those connections. -This section reviews the basic options for binding queues to exchanges. - -You can bind a queue to a `DirectExchange` with a fixed routing key, as the following example shows: - -==== -[source,java] ----- -new Binding(someQueue, someDirectExchange, "foo.bar"); ----- -==== - -You can bind a queue to a `TopicExchange` with a routing pattern, as the following example shows: - -==== -[source,java] ----- -new Binding(someQueue, someTopicExchange, "foo.*"); ----- -==== - -You can bind a queue to a `FanoutExchange` with no routing key, as the following example shows: - -==== -[source,java] ----- -new Binding(someQueue, someFanoutExchange); ----- -==== - -We also provide a `BindingBuilder` to facilitate a "`fluent API`" style, as the following example shows: - -==== -[source,java] ----- -Binding b = BindingBuilder.bind(someQueue).to(someTopicExchange).with("foo.*"); ----- -==== - -NOTE: For clarity, the preceding example shows the `BindingBuilder` class, but this style works well when using a static import for the 'bind()' method. - -By itself, an instance of the `Binding` class only holds the data about a connection. -In other words, it is not an "`active`" component. -However, as you will see later in <>, the `AmqpAdmin` class can use `Binding` instances to actually trigger the binding actions on the broker. -Also, as you can see in that same section, you can define the `Binding` instances by using Spring's `@Bean` annotations within `@Configuration` classes. -There is also a convenient base class that further simplifies that approach for generating AMQP-related bean definitions and recognizes the queues, exchanges, and bindings so that they are all declared on the AMQP broker upon application startup. - -The `AmqpTemplate` is also defined within the core package. -As one of the main components involved in actual AMQP messaging, it is discussed in detail in its own section (see <>). - -[[connections]] -==== Connection and Resource Management - -Whereas the AMQP model we described in the previous section is generic and applicable to all implementations, when we get into the management of resources, the details are specific to the broker implementation. -Therefore, in this section, we focus on code that exists only within our "`spring-rabbit`" module since, at this point, RabbitMQ is the only supported implementation. - -The central component for managing a connection to the RabbitMQ broker is the `ConnectionFactory` interface. -The responsibility of a `ConnectionFactory` implementation is to provide an instance of `org.springframework.amqp.rabbit.connection.Connection`, which is a wrapper for `com.rabbitmq.client.Connection`. - -[[choosing-factory]] -===== Choosing a Connection Factory - -There are three connection factories to chose from - -* `PooledChannelConnectionFactory` -* `ThreadChannelConnectionFactory` -* `CachingConnectionFactory` - -The first two were added in version 2.3. - -For most use cases, the `CachingConnectionFactory` should be used. -The `ThreadChannelConnectionFactory` can be used if you want to ensure strict message ordering without the need to use <>. -The `PooledChannelConnectionFactory` is similar to the `CachingConnectionFactory` in that it uses a single connection and a pool of channels. -It's implementation is simpler but it doesn't support correlated publisher confirmations. - -Simple publisher confirmations are supported by all three factories. - -When configuring a `RabbitTemplate` to use a <>, you can now, starting with version 2.3.2, configure the publishing connection factory to be a different type. -By default, the publishing factory is the same type and any properties set on the main factory are also propagated to the publishing factory. - -====== `PooledChannelConnectionFactory` - -This factory manages a single connection and two pools of channels, based on the Apache Pool2. -One pool is for transactional channels, the other is for non-transactional channels. -The pools are `GenericObjectPool` s with default configuration; a callback is provided to configure the pools; refer to the Apache documentation for more information. - -The Apache `commons-pool2` jar must be on the class path to use this factory. - -==== -[source, java] ----- -@Bean -PooledChannelConnectionFactory pcf() throws Exception { - ConnectionFactory rabbitConnectionFactory = new ConnectionFactory(); - rabbitConnectionFactory.setHost("localhost"); - PooledChannelConnectionFactory pcf = new PooledChannelConnectionFactory(rabbitConnectionFactory); - pcf.setPoolConfigurer((pool, tx) -> { - if (tx) { - // configure the transactional pool - } - else { - // configure the non-transactional pool - } - }); - return pcf; -} ----- -==== - -====== `ThreadChannelConnectionFactory` - -This factory manages a single connection and two `ThreadLocal` s, one for transactional channels, the other for non-transactional channels. -This factory ensures that all operations on the same thread use the same channel (as long as it remains open). -This facilitates strict message ordering without the need for <>. -To avoid memory leaks, if your application uses many short-lived threads, you must call the factory's `closeThreadChannel()` to release the channel resource. -Starting with version 2.3.7, a thread can transfer its channel(s) to another thread. -See <> for more information. - -====== `CachingConnectionFactory` - -The third implementation provided is the `CachingConnectionFactory`, which, by default, establishes a single connection proxy that can be shared by the application. -Sharing of the connection is possible since the "`unit of work`" for messaging with AMQP is actually a "`channel`" (in some ways, this is similar to the relationship between a connection and a session in JMS). -The connection instance provides a `createChannel` method. -The `CachingConnectionFactory` implementation supports caching of those channels, and it maintains separate caches for channels based on whether they are transactional. -When creating an instance of `CachingConnectionFactory`, you can provide the 'hostname' through the constructor. -You should also provide the 'username' and 'password' properties. -To configure the size of the channel cache (the default is 25), you can call the -`setChannelCacheSize()` method. - -Starting with version 1.3, you can configure the `CachingConnectionFactory` to cache connections as well as only channels. -In this case, each call to `createConnection()` creates a new connection (or retrieves an idle one from the cache). -Closing a connection returns it to the cache (if the cache size has not been reached). -Channels created on such connections are also cached. -The use of separate connections might be useful in some environments, such as consuming from an HA cluster, in -conjunction with a load balancer, to connect to different cluster members, and others. -To cache connections, set the `cacheMode` to `CacheMode.CONNECTION`. - -NOTE: This does not limit the number of connections. -Rather, it specifies how many idle open connections are allowed. - -Starting with version 1.5.5, a new property called `connectionLimit` is provided. -When this property is set, it limits the total number of connections allowed. -When set, if the limit is reached, the `channelCheckoutTimeLimit` is used to wait for a connection to become idle. -If the time is exceeded, an `AmqpTimeoutException` is thrown. - -[IMPORTANT] -====== -When the cache mode is `CONNECTION`, automatic declaration of queues and others -(See <>) is NOT supported. - -Also, at the time of this writing, the `amqp-client` library by default creates a fixed thread pool for each connection (default size: `Runtime.getRuntime().availableProcessors() * 2` threads). -When using a large number of connections, you should consider setting a custom `executor` on the `CachingConnectionFactory`. -Then, the same executor can be used by all connections and its threads can be shared. -The executor's thread pool should be unbounded or set appropriately for the expected use (usually, at least one thread per connection). -If multiple channels are created on each connection, the pool size affects the concurrency, so a variable (or simple cached) thread pool executor would be most suitable. -====== - -It is important to understand that the cache size is (by default) not a limit but is merely the number of channels that can be cached. -With a cache size of, say, 10, any number of channels can actually be in use. -If more than 10 channels are being used and they are all returned to the cache, 10 go in the cache. -The remainder are physically closed. - -Starting with version 1.6, the default channel cache size has been increased from 1 to 25. -In high volume, multi-threaded environments, a small cache means that channels are created and closed at a high rate. -Increasing the default cache size can avoid this overhead. -You should monitor the channels in use through the RabbitMQ Admin UI and consider increasing the cache size further if you -see many channels being created and closed. -The cache grows only on-demand (to suit the concurrency requirements of the application), so this change does not -impact existing low-volume applications. - -Starting with version 1.4.2, the `CachingConnectionFactory` has a property called `channelCheckoutTimeout`. -When this property is greater than zero, the `channelCacheSize` becomes a limit on the number of channels that can be created on a connection. -If the limit is reached, calling threads block until a channel is available or this timeout is reached, in which case a `AmqpTimeoutException` is thrown. - -WARNING: Channels used within the framework (for example, -`RabbitTemplate`) are reliably returned to the cache. -If you create channels outside of the framework, (for example, -by accessing the connections directly and invoking `createChannel()`), you must return them (by closing) reliably, perhaps in a `finally` block, to avoid running out of channels. - -The following example shows how to create a new `connection`: - -==== -[source,java] ----- -CachingConnectionFactory connectionFactory = new CachingConnectionFactory("somehost"); -connectionFactory.setUsername("guest"); -connectionFactory.setPassword("guest"); - -Connection connection = connectionFactory.createConnection(); ----- -==== - -==== -When using XML, the configuration might look like the following example: - -[source,xml] ----- - - - - - ----- -==== - -NOTE: There is also a `SingleConnectionFactory` implementation that is available only in the unit test code of the framework. -It is simpler than `CachingConnectionFactory`, since it does not cache channels, but it is not intended for practical usage outside of simple tests due to its lack of performance and resilience. -If you need to implement your own `ConnectionFactory` for some reason, the `AbstractConnectionFactory` base class may provide a nice starting point. - -A `ConnectionFactory` can be created quickly and conveniently by using the rabbit namespace, as follows: - -==== -[source,xml] ----- - ----- -==== - -In most cases, this approach is preferable, since the framework can choose the best defaults for you. -The created instance is a `CachingConnectionFactory`. -Keep in mind that the default cache size for channels is 25. -If you want more channels to be cached, set a larger value by setting the 'channelCacheSize' property. -In XML it would look like as follows: - -==== -[source,xml] ----- - - - - - - ----- -==== - -Also, with the namespace, you can add the 'channel-cache-size' attribute, as follows: - -==== -[source,xml] ----- - ----- -==== - -The default cache mode is `CHANNEL`, but you can configure it to cache connections instead. -In the following example, we use `connection-cache-size`: - -==== -[source,xml] ----- - ----- -==== - -You can provide host and port attributes by using the namespace, as follows: - -==== -[source,xml] ----- - ----- -==== - -Alternatively, if running in a clustered environment, you can use the addresses attribute, as follows: - -==== -[source,xml] ----- - ----- -==== - -See <> for information about `address-shuffle-mode`. - -The following example with a custom thread factory that prefixes thread names with `rabbitmq-`: - -==== -[source, xml] ----- - - - - - - ----- -==== - -===== AddressResolver - -Starting with version 2.1.15, you can now use an `AddressResolver` to resolve the connection address(es). -This will override any settings of the `addresses` and `host/port` properties. - -===== Naming Connections - -Starting with version 1.7, a `ConnectionNameStrategy` is provided for the injection into the `AbstractionConnectionFactory`. -The generated name is used for the application-specific identification of the target RabbitMQ connection. -The connection name is displayed in the management UI if the RabbitMQ server supports it. -This value does not have to be unique and cannot be used as a connection identifier -- for example, in HTTP API requests. -This value is supposed to be human-readable and is a part of `ClientProperties` under the `connection_name` key. -You can use a simple Lambda, as follows: - -==== -[source, java] ----- -connectionFactory.setConnectionNameStrategy(connectionFactory -> "MY_CONNECTION"); ----- -==== - -The `ConnectionFactory` argument can be used to distinguish target connection names by some logic. -By default, the `beanName` of the `AbstractConnectionFactory`, a hex string representing the object, and an internal counter are used to generate the `connection_name`. -The `` namespace component is also supplied with the `connection-name-strategy` attribute. - -An implementation of `SimplePropertyValueConnectionNameStrategy` sets the connection name to an application property. -You can declare it as a `@Bean` and inject it into the connection factory, as the following example shows: - -==== -[source, java] ----- -@Bean -public SimplePropertyValueConnectionNameStrategy cns() { - return new SimplePropertyValueConnectionNameStrategy("spring.application.name"); -} - -@Bean -public ConnectionFactory rabbitConnectionFactory(ConnectionNameStrategy cns) { - CachingConnectionFactory connectionFactory = new CachingConnectionFactory(); - ... - connectionFactory.setConnectionNameStrategy(cns); - return connectionFactory; -} ----- -==== - -The property must exist in the application context's `Environment`. - -NOTE: When using Spring Boot and its autoconfigured connection factory, you need only declare the `ConnectionNameStrategy` `@Bean`. -Boot auto-detects the bean and wires it into the factory. - -===== Blocked Connections and Resource Constraints - -The connection might be blocked for interaction from the broker that corresponds to the https://www.rabbitmq.com/memory.html[Memory Alarm]. -Starting with version 2.0, the `org.springframework.amqp.rabbit.connection.Connection` can be supplied with `com.rabbitmq.client.BlockedListener` instances to be notified for connection blocked and unblocked events. -In addition, the `AbstractConnectionFactory` emits a `ConnectionBlockedEvent` and `ConnectionUnblockedEvent`, respectively, through its internal `BlockedListener` implementation. -These let you provide application logic to react appropriately to problems on the broker and (for example) take some corrective actions. - -IMPORTANT: When the application is configured with a single `CachingConnectionFactory`, as it is by default with Spring Boot auto-configuration, the application stops working when the connection is blocked by the Broker. -And when it is blocked by the Broker, any of its clients stop to work. -If we have producers and consumers in the same application, we may end up with a deadlock when producers are blocking the connection (because there are no resources on the Broker any more) and consumers cannot free them (because the connection is blocked). -To mitigate the problem, we suggest having one more separate `CachingConnectionFactory` instance with the same options -- one for producers and one for consumers. -A separate `CachingConnectionFactory` is not possible for transactional producers that execute on a consumer thread, since they should reuse the `Channel` associated with the consumer transactions. - -Starting with version 2.0.2, the `RabbitTemplate` has a configuration option to automatically use a second connection factory, unless transactions are being used. -See <> for more information. -The `ConnectionNameStrategy` for the publisher connection is the same as the primary strategy with `.publisher` appended to the result of calling the method. - -Starting with version 1.7.7, an `AmqpResourceNotAvailableException` is provided, which is thrown when `SimpleConnection.createChannel()` cannot create a `Channel` (for example, because the `channelMax` limit is reached and there are no available channels in the cache). -You can use this exception in the `RetryPolicy` to recover the operation after some back-off. - -[[connection-factory]] -===== Configuring the Underlying Client Connection Factory - -The `CachingConnectionFactory` uses an instance of the Rabbit client `ConnectionFactory`. -A number of configuration properties are passed through (`host`, `port`, `userName`, `password`, `requestedHeartBeat`, and `connectionTimeout` for example) when setting the equivalent property on the `CachingConnectionFactory`. -To set other properties (`clientProperties`, for example), you can define an instance of the Rabbit factory and provide a reference to it by using the appropriate constructor of the `CachingConnectionFactory`. -When using the namespace (<>), you need to provide a reference to the configured factory in the `connection-factory` attribute. -For convenience, a factory bean is provided to assist in configuring the connection factory in a Spring application context, as discussed in <>. - -==== -[source,xml] ----- - ----- -==== - -NOTE: The 4.0.x client enables automatic recovery by default. -While compatible with this feature, Spring AMQP has its own recovery mechanisms and the client recovery feature generally is not needed. -We recommend disabling `amqp-client` automatic recovery, to avoid getting `AutoRecoverConnectionNotCurrentlyOpenException` instances when the broker is available but the connection has not yet recovered. -You may notice this exception, for example, when a `RetryTemplate` is configured in a `RabbitTemplate`, even when failing over to another broker in a cluster. -Since the auto-recovering connection recovers on a timer, the connection may be recovered more quickly by using Spring AMQP's recovery mechanisms. -Starting with version 1.7.1, Spring AMQP disables `amqp-client` automatic recovery unless you explicitly create your own RabbitMQ connection factory and provide it to the `CachingConnectionFactory`. -RabbitMQ `ConnectionFactory` instances created by the `RabbitConnectionFactoryBean` also have the option disabled by default. - -[[rabbitconnectionfactorybean-configuring-ssl]] -===== `RabbitConnectionFactoryBean` and Configuring SSL - -Starting with version 1.4, a convenient `RabbitConnectionFactoryBean` is provided to enable convenient configuration of SSL properties on the underlying client connection factory by using dependency injection. -Other setters delegate to the underlying factory. -Previously, you had to configure the SSL options programmatically. -The following example shows how to configure a `RabbitConnectionFactoryBean`: - -==== -[source,java,role=primary] -.Java ----- -@Bean -RabbitConnectionFactoryBean rabbitConnectionFactory() { - RabbitConnectionFactoryBean factoryBean = new RabbitConnectionFactoryBean(); - factoryBean.setUseSSL(true); - factoryBean.setSslPropertiesLocation(new ClassPathResource("secrets/rabbitSSL.properties")); - return factoryBean; -} - -@Bean -CachingConnectionFactory connectionFactory(ConnectionFactory rabbitConnectionFactory) { - CachingConnectionFactory ccf = new CachingConnectionFactory(rabbitConnectionFactory); - ccf.setHost("..."); - // ... - return ccf; -} ----- -[source,properties,role=secondary] -.Boot application.properties ----- -spring.rabbitmq.ssl.enabled:true -spring.rabbitmq.ssl.keyStore=... -spring.rabbitmq.ssl.keyStoreType=jks -spring.rabbitmq.ssl.keyStorePassword=... -spring.rabbitmq.ssl.trustStore=... -spring.rabbitmq.ssl.trustStoreType=jks -spring.rabbitmq.ssl.trustStorePassword=... -spring.rabbitmq.host=... -... ----- -[source,xml,role=secondary] -.XML ----- - - - - - - ----- -==== - -See the https://www.rabbitmq.com/ssl.html[RabbitMQ Documentation] for information about configuring SSL. -Omit the `keyStore` and `trustStore` configuration to connect over SSL without certificate validation. -The next example shows how you can provide key and trust store configuration. - -The `sslPropertiesLocation` property is a Spring `Resource` pointing to a properties file containing the following keys: - -==== -[source] ----- -keyStore=file:/secret/keycert.p12 -trustStore=file:/secret/trustStore -keyStore.passPhrase=secret -trustStore.passPhrase=secret ----- -==== - -The `keyStore` and `truststore` are Spring `Resources` pointing to the stores. -Typically this properties file is secured by the operating system with the application having read access. - -Starting with Spring AMQP version 1.5,you can set these properties directly on the factory bean. -If both discrete properties and `sslPropertiesLocation` is provided, properties in the latter override the -discrete values. - -IMPORTANT: Starting with version 2.0, the server certificate is validated by default because it is more secure. -If you wish to skip this validation for some reason, set the factory bean's `skipServerCertificateValidation` property to `true`. -Starting with version 2.1, the `RabbitConnectionFactoryBean` now calls `enableHostnameVerification()` by default. -To revert to the previous behavior, set the `enableHostnameVerification` property to `false`. - -IMPORTANT: Starting with version 2.2.5, the factory bean will always use TLS v1.2 by default; previously, it used v1.1 in some cases and v1.2 in others (depending on other properties). -If you need to use v1.1 for some reason, set the `sslAlgorithm` property: `setSslAlgorithm("TLSv1.1")`. - -[[cluster]] -===== Connecting to a Cluster - -To connect to a cluster, configure the `addresses` property on the `CachingConnectionFactory`: - -==== -[source, java] ----- -@Bean -public CachingConnectionFactory ccf() { - CachingConnectionFactory ccf = new CachingConnectionFactory(); - ccf.setAddresses("host1:5672,host2:5672,host3:5672"); - return ccf; -} ----- -==== - -Starting with version 3.0, the underlying connection factory will attempt to connect to a host, by choosing a random address, whenever a new connection is established. -To revert to the previous behavior of attempting to connect from first to last, set the `addressShuffleMode` property to `AddressShuffleMode.NONE`. - -Starting with version 2.3, the `INORDER` shuffle mode was added, which means the first address is moved to the end after a connection is created. -You may wish to use this mode with the https://github.com/rabbitmq/rabbitmq-sharding[RabbitMQ Sharding Plugin] with `CacheMode.CONNECTION` and suitable concurrency if you wish to consume from all shards on all nodes. - -==== -[source, java] ----- -@Bean -public CachingConnectionFactory ccf() { - CachingConnectionFactory ccf = new CachingConnectionFactory(); - ccf.setAddresses("host1:5672,host2:5672,host3:5672"); - ccf.setAddressShuffleMode(AddressShuffleMode.INORDER); - return ccf; -} ----- -==== - -[[routing-connection-factory]] -===== Routing Connection Factory - -Starting with version 1.3, the `AbstractRoutingConnectionFactory` has been introduced. -This factory provides a mechanism to configure mappings for several `ConnectionFactories` and determine a target `ConnectionFactory` by some `lookupKey` at runtime. -Typically, the implementation checks a thread-bound context. -For convenience, Spring AMQP provides the `SimpleRoutingConnectionFactory`, which gets the current thread-bound `lookupKey` from the `SimpleResourceHolder`. -The following examples shows how to configure a `SimpleRoutingConnectionFactory` in both XML and Java: - -==== -[source,xml] ----- - - - - - - - - - - ----- - -[source,java] ----- -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()); - } - -} ----- -==== - -It is important to unbind the resource after use. -For more information, see the https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.html[JavaDoc] for `AbstractRoutingConnectionFactory`. - -Starting with version 1.4, `RabbitTemplate` supports the SpEL `sendConnectionFactorySelectorExpression` and `receiveConnectionFactorySelectorExpression` properties, which are evaluated on each AMQP protocol interaction operation (`send`, `sendAndReceive`, `receive`, or `receiveAndReply`), resolving to a `lookupKey` value for the provided `AbstractRoutingConnectionFactory`. -You can use bean references, such as `@vHostResolver.getVHost(#root)` in the expression. -For `send` operations, the message to be sent is the root evaluation object. -For `receive` operations, the `queueName` is the root evaluation object. - -The routing algorithm is as follows: If the selector expression is `null` or is evaluated to `null` or the provided `ConnectionFactory` is not an instance of `AbstractRoutingConnectionFactory`, everything works as before, relying on the provided `ConnectionFactory` implementation. -The same occurs if the evaluation result is not `null`, but there is no target `ConnectionFactory` for that `lookupKey` and the `AbstractRoutingConnectionFactory` is configured with `lenientFallback = true`. -In the case of an `AbstractRoutingConnectionFactory`, it does fallback to its `routing` implementation based on `determineCurrentLookupKey()`. -However, if `lenientFallback = false`, an `IllegalStateException` is thrown. - -The namespace support also provides the `send-connection-factory-selector-expression` and `receive-connection-factory-selector-expression` attributes on the `` component. - -Also, starting with version 1.4, you can configure a routing connection factory in a listener container. -In that case, the list of queue names is used as the lookup key. -For example, if you configure the container with `setQueueNames("thing1", "thing2")`, the lookup key is `[thing1,thing]"` (note that there is no space in the key). - -Starting with version 1.6.9, you can add a qualifier to the lookup key by using `setLookupKeyQualifier` on the listener container. -Doing so enables, for example, listening to queues with the same name but in a different virtual host (where you would have a connection factory for each). - -For example, with lookup key qualifier `thing1` and a container listening to queue `thing2`, the lookup key you could register the target connection factory with could be `thing1[thing2]`. - -IMPORTANT: The target (and default, if provided) connection factories must have the same settings for publisher confirms and returns. -See <>. - -Starting with version 2.4.4, this validation can be disabled. -If you have a case that the values between confirms and returns need to be unequal, you can use `AbstractRoutingConnectionFactory#setConsistentConfirmsReturns` to turn of the validation. -Note that the first connection factory added to `AbstractRoutingConnectionFactory` will determine the general values of `confirms` and `returns`. - -It may be useful if you have a case that certain messages you would to check confirms/returns and others you don't. -For example: - -==== -[source, java] ----- -@Bean -public RabbitTemplate rabbitTemplate() { - final com.rabbitmq.client.ConnectionFactory cf = new com.rabbitmq.client.ConnectionFactory(); - cf.setHost("localhost"); - cf.setPort(5672); - - CachingConnectionFactory cachingConnectionFactory = new CachingConnectionFactory(cf); - cachingConnectionFactory.setPublisherConfirmType(CachingConnectionFactory.ConfirmType.CORRELATED); - - PooledChannelConnectionFactory pooledChannelConnectionFactory = new PooledChannelConnectionFactory(cf); - - final Map connectionFactoryMap = new HashMap<>(2); - connectionFactoryMap.put("true", cachingConnectionFactory); - connectionFactoryMap.put("false", pooledChannelConnectionFactory); - - final AbstractRoutingConnectionFactory routingConnectionFactory = new SimpleRoutingConnectionFactory(); - routingConnectionFactory.setConsistentConfirmsReturns(false); - routingConnectionFactory.setDefaultTargetConnectionFactory(pooledChannelConnectionFactory); - routingConnectionFactory.setTargetConnectionFactories(connectionFactoryMap); - - final RabbitTemplate rabbitTemplate = new RabbitTemplate(routingConnectionFactory); - - final Expression sendExpression = new SpelExpressionParser().parseExpression( - "messageProperties.headers['x-use-publisher-confirms'] ?: false"); - rabbitTemplate.setSendConnectionFactorySelectorExpression(sendExpression); -} ----- -==== - -This way messages with the header `x-use-publisher-confirms: true` will be sent through the caching connection and you can ensure the message delivery. -See <> for more information about ensuring message delivery. - -[[queue-affinity]] -===== Queue Affinity and the `LocalizedQueueConnectionFactory` - -When using HA queues in a cluster, for the best performance, you may want to connect to the physical broker -where the lead queue resides. -The `CachingConnectionFactory` can be configured with multiple broker addresses. -This is to fail over and the client attempts to connect in accordance with the configured `AddressShuffleMode` order. -The `LocalizedQueueConnectionFactory` uses the REST API provided by the management plugin to determine which node is the lead for the queue. -It then creates (or retrieves from a cache) a `CachingConnectionFactory` that connects to just that node. -If the connection fails, the new lead node is determined and the consumer connects to it. -The `LocalizedQueueConnectionFactory` is configured with a default connection factory, in case the physical location of the queue cannot be determined, in which case it connects as normal to the cluster. - -The `LocalizedQueueConnectionFactory` is a `RoutingConnectionFactory` and the `SimpleMessageListenerContainer` uses the queue names as the lookup key as discussed in <> above. - -NOTE: For this reason (the use of the queue name for the lookup), the `LocalizedQueueConnectionFactory` can only be used if the container is configured to listen to a single queue. - -NOTE: The RabbitMQ management plugin must be enabled on each node. - -CAUTION: This connection factory is intended for long-lived connections, such as those used by the `SimpleMessageListenerContainer`. -It is not intended for short connection use, such as with a `RabbitTemplate` because of the overhead of invoking the REST API before making the connection. -Also, for publish operations, the queue is unknown, and the message is published to all cluster members anyway, so the logic of looking up the node has little value. - -The following example configuration shows how to configure the factories: - -==== -[source, java] ----- -@Autowired -private ConfigurationProperties props; - -@Bean -public CachingConnectionFactory 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 LocalizedQueueConnectionFactory queueAffinityCF( - @Qualifier("defaultConnectionFactory") ConnectionFactory defaultCF) { - return new LocalizedQueueConnectionFactory(defaultCF, - StringUtils.commaDelimitedListToStringArray(this.props.getAddresses()), - StringUtils.commaDelimitedListToStringArray(this.props.getAdminUris()), - StringUtils.commaDelimitedListToStringArray(this.props.getNodes()), - this.props.getVirtualHost(), this.props.getUsername(), this.props.getPassword(), - false, null); -} ----- -==== - -Notice that the first three parameters are arrays of `addresses`, `adminUris`, and `nodes`. -These are positional in that, when a container attempts to connect to a queue, it uses the admin API to determine which node is the lead for the queue and connects to the address in the same array position as that node. - -IMPORTANT: Starting with version 3.0, the RabbitMQ `http-client` is no longer used to access the Rest API. -Instead, by default, the `WebClient` from Spring Webflux is used if `spring-webflux` is on the class path; otherwise a `RestTemplate` is used. - -To add `WebFlux` to the class path: - -.Maven -==== -[source,xml,subs="+attributes"] ----- - - org.springframework.amqp - spring-rabbit - ----- -==== -.Gradle -==== -[source,groovy,subs="+attributes"] ----- -compile 'org.springframework.amqp:spring-rabbit' ----- -==== - -You can also use other REST technology by implementing `LocalizedQueueConnectionFactory.NodeLocator` and overriding its `createClient, ``restCall`, and optionally, `close` methods. - -==== -[source, java] ----- -lqcf.setNodeLocator(new NodeLocator() { - - @Override - public MyClient createClient(String userName, String password) { - ... - } - - @Override - public HashMap restCall(MyClient client, URI uri) { - ... - }); - -}); ----- -==== - -The framework provides the `WebFluxNodeLocator` and `RestTemplateNodeLocator`, with the default as discussed above. - -[[cf-pub-conf-ret]] -===== Publisher Confirms and Returns - -Confirmed (with correlation) and returned messages are supported by setting the `CachingConnectionFactory` property `publisherConfirmType` to `ConfirmType.CORRELATED` and the `publisherReturns` property to 'true'. - -When these options are set, `Channel` instances created by the factory are wrapped in an `PublisherCallbackChannel`, which is used to facilitate the callbacks. -When such a channel is obtained, the client can register a `PublisherCallbackChannel.Listener` with the `Channel`. -The `PublisherCallbackChannel` implementation contains logic to route a confirm or return to the appropriate listener. -These features are explained further in the following sections. - -See also <> and `simplePublisherConfirms` in <>. - -TIP: For some more background information, see the blog post by the RabbitMQ team titled https://www.rabbitmq.com/blog/2011/02/10/introducing-publisher-confirms/[Introducing Publisher Confirms]. - -[[connection-channel-listeners]] -===== Connection and Channel Listeners - -The connection factory supports registering `ConnectionListener` and `ChannelListener` implementations. -This allows you to receive notifications for connection and channel related events. -(A `ConnectionListener` is used by the `RabbitAdmin` to perform declarations when the connection is established - see <> for more information). -The following listing shows the `ConnectionListener` interface definition: - -==== -[source, java] ----- -@FunctionalInterface -public interface ConnectionListener { - - void onCreate(Connection connection); - - default void onClose(Connection connection) { - } - - default void onShutDown(ShutdownSignalException signal) { - } - -} ----- -==== - -Starting with version 2.0, the `org.springframework.amqp.rabbit.connection.Connection` object can be supplied with `com.rabbitmq.client.BlockedListener` instances to be notified for connection blocked and unblocked events. -The following example shows the ChannelListener interface definition: - -==== -[source, java] ----- -@FunctionalInterface -public interface ChannelListener { - - void onCreate(Channel channel, boolean transactional); - - default void onShutDown(ShutdownSignalException signal) { - } - -} ----- -==== - -See <> for one scenario where you might want to register a `ChannelListener`. - -[[channel-close-logging]] -===== Logging Channel Close Events - -Version 1.5 introduced a mechanism to enable users to control logging levels. - -The `AbstractConnectionFactory` uses a default strategy to log channel closures as follows: - -* Normal channel closes (200 OK) are not logged. -* If a channel is closed due to a failed passive queue declaration, it is logged at DEBUG level. -* If a channel is closed because the `basic.consume` is refused due to an exclusive consumer condition, it is logged at -DEBUG level (since 3.1, previously INFO). -* All others are logged at ERROR level. - -To modify this behavior, you can inject a custom `ConditionalExceptionLogger` into the -`CachingConnectionFactory` in its `closeExceptionLogger` property. - -Also, the `AbstractConnectionFactory.DefaultChannelCloseLogger` is now public, allowing it to be sub classed. - -See also <>. - -[[runtime-cache-properties]] -===== Runtime Cache Properties - -Staring with version 1.6, the `CachingConnectionFactory` now provides cache statistics through the `getCacheProperties()` -method. -These statistics can be used to tune the cache to optimize it in production. -For example, the high water marks can be used to determine whether the cache size should be increased. -If it equals the cache size, you might want to consider increasing further. -The following table describes the `CacheMode.CHANNEL` properties: - -.Cache properties for CacheMode.CHANNEL -[cols="2l,4", options="header"] -|=== -|Property - -|Meaning - -|connectionName - -|The name of the connection generated by the `ConnectionNameStrategy`. - -|channelCacheSize - -|The currently configured maximum channels that are allowed to be idle. - -|localPort - -|The local port for the connection (if available). -This can be used to correlate with connections and channels on the RabbitMQ Admin UI. - -|idleChannelsTx - -|The number of transactional channels that are currently idle (cached). - -|idleChannelsNotTx - -|The number of non-transactional channels that are currently idle (cached). - -|idleChannelsTxHighWater - -|The maximum number of transactional channels that have been concurrently idle (cached). - -|idleChannelsNotTxHighWater - -|The maximum number of non-transactional channels have been concurrently idle (cached). - -|=== - -The following table describes the `CacheMode.CONNECTION` properties: - -.Cache properties for CacheMode.CONNECTION -[cols="2l,4", options="header"] -|=== -|Property - -|Meaning - -|connectionName: - -|The name of the connection generated by the `ConnectionNameStrategy`. - -|openConnections - -|The number of connection objects representing connections to brokers. - -|channelCacheSize - -|The currently configured maximum channels that are allowed to be idle. - -|connectionCacheSize - -|The currently configured maximum connections that are allowed to be idle. - -|idleConnections - -|The number of connections that are currently idle. - -|idleConnectionsHighWater - -|The maximum number of connections that have been concurrently idle. - -|idleChannelsTx: - -|The number of transactional channels that are currently idle (cached) for this connection. -You can use the `localPort` part of the property name to correlate with connections and channels on the RabbitMQ Admin UI. - -|idleChannelsNotTx: - -|The number of non-transactional channels that are currently idle (cached) for this connection. -The `localPort` part of the property name can be used to correlate with connections and channels on the RabbitMQ Admin UI. - -|idleChannelsTxHighWater: - -|The maximum number of transactional channels that have been concurrently idle (cached). -The localPort part of the property name can be used to correlate with connections and channels on the RabbitMQ Admin UI. - -|idleChannelsNotTxHighWater: - -|The maximum number of non-transactional channels have been concurrently idle (cached). -You can use the `localPort` part of the property name to correlate with connections and channels on the RabbitMQ Admin UI. - -|=== - -The `cacheMode` property (`CHANNEL` or `CONNECTION`) is also included. - -.JVisualVM Example -image::images/cacheStats.png[align="center"] - -[[auto-recovery]] -===== RabbitMQ Automatic Connection/Topology recovery - -Since the first version of Spring AMQP, the framework has provided its own connection and channel recovery in the event of a broker failure. -Also, as discussed in <>, the `RabbitAdmin` re-declares any infrastructure beans (queues and others) when the connection is re-established. -It therefore does not rely on the https://www.rabbitmq.com/api-guide.html#recovery[auto-recovery] that is now provided by the `amqp-client` library. -The `amqp-client`, has auto recovery enabled by default. -There are some incompatibilities between the two recovery mechanisms so, by default, Spring sets the `automaticRecoveryEnabled` property on the underlying `RabbitMQ connectionFactory` to `false`. -Even if the property is `true`, Spring effectively disables it, by immediately closing any recovered connections. - -IMPORTANT: By default, only elements (queues, exchanges, bindings) that are defined as beans will be re-declared after a connection failure. -See <> for how to change that behavior. - -[[custom-client-props]] -==== Adding Custom Client Connection Properties - -The `CachingConnectionFactory` now lets you access the underlying connection factory to allow, for example, -setting custom client properties. -The following example shows how to do so: - -[source, java] ----- -connectionFactory.getRabbitConnectionFactory().getClientProperties().put("thing1", "thing2"); ----- - -These properties appear in the RabbitMQ Admin UI when viewing the connection. - -[[amqp-template]] -==== `AmqpTemplate` - -As with many other high-level abstractions provided by the Spring Framework and related projects, Spring AMQP provides a "`template`" that plays a central role. -The interface that defines the main operations is called `AmqpTemplate`. -Those operations cover the general behavior for sending and receiving messages. -In other words, they are not unique to any implementation -- hence the "`AMQP`" in the name. -On the other hand, there are implementations of that interface that are tied to implementations of the AMQP protocol. -Unlike JMS, which is an interface-level API itself, AMQP is a wire-level protocol. -The implementations of that protocol provide their own client libraries, so each implementation of the template interface depends on a particular client library. -Currently, there is only a single implementation: `RabbitTemplate`. -In the examples that follow, we often use an `AmqpTemplate`. -However, when you look at the configuration examples or any code excerpts where the template is instantiated or setters are invoked, you can see the implementation type (for example, `RabbitTemplate`). - -As mentioned earlier, the `AmqpTemplate` interface defines all of the basic operations for sending and receiving messages. -We will explore message sending and reception, respectively, in <> and <>. - -See also <>. - -[[template-retry]] -===== Adding Retry Capabilities - -Starting with version 1.3, you can now configure the `RabbitTemplate` to use a `RetryTemplate` to help with handling problems with broker connectivity. -See the https://github.com/spring-projects/spring-retry[spring-retry] project for complete information. -The following is only one example that uses an exponential back off policy and the default `SimpleRetryPolicy`, which makes three tries before throwing the exception to the caller. - -The following example uses the XML namespace: - -==== -[source,xml] ----- - - - - - - - - - - - ----- -==== - -The following example uses the `@Configuration` annotation in Java: - -==== -[source,java] ----- -@Bean -public RabbitTemplate 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; -} ----- -==== - -Starting with version 1.4, in addition to the `retryTemplate` property, the `recoveryCallback` option is supported on the `RabbitTemplate`. -It is used as a second argument for the `RetryTemplate.execute(RetryCallback retryCallback, RecoveryCallback recoveryCallback)`. - -NOTE: The `RecoveryCallback` is somewhat limited, in that the retry context contains only the `lastThrowable` field. -For more sophisticated use cases, you should use an external `RetryTemplate` so that you can convey additional information to the `RecoveryCallback` through the context's attributes. -The following example shows how to do so: - -==== -[source,java] ----- -retryTemplate.execute( - new RetryCallback() { - - @Override - public Object doWithRetry(RetryContext context) throws Exception { - context.setAttribute("message", message); - return rabbitTemplate.convertAndSend(exchange, routingKey, message); - } - - }, new RecoveryCallback() { - - @Override - public Object recover(RetryContext context) throws Exception { - Object message = context.getAttribute("message"); - Throwable t = context.getLastThrowable(); - // Do something with message - return null; - } - }); -} ----- -==== - -In this case, you would *not* inject a `RetryTemplate` into the `RabbitTemplate`. - -[[publishing-is-async]] -===== Publishing is Asynchronous -- How to Detect Successes and Failures - -Publishing messages is an asynchronous mechanism and, by default, messages that cannot be routed are dropped by RabbitMQ. -For successful publishing, you can receive an asynchronous confirm, as described in <>. -Consider two failure scenarios: - -* Publish to an exchange but there is no matching destination queue. -* Publish to a non-existent exchange. - -The first case is covered by publisher returns, as described in <>. - -For the second case, the message is dropped and no return is generated. -The underlying channel is closed with an exception. -By default, this exception is logged, but you can register a `ChannelListener` with the `CachingConnectionFactory` to obtain notifications of such events. -The following example shows how to add a `ConnectionListener`: - -==== -[source, java] ----- -this.connectionFactory.addConnectionListener(new ConnectionListener() { - - @Override - public void onCreate(Connection connection) { - } - - @Override - public void onShutDown(ShutdownSignalException signal) { - ... - } - -}); ----- -==== - -You can examine the signal's `reason` property to determine the problem that occurred. - -To detect the exception on the sending thread, you can `setChannelTransacted(true)` on the `RabbitTemplate` and the exception is detected on the `txCommit()`. -However, *transactions significantly impede performance*, so consider this carefully before enabling transactions for just this one use case. - -[[template-confirms]] -===== Correlated Publisher Confirms and Returns - -The `RabbitTemplate` implementation of `AmqpTemplate` supports publisher confirms and returns. - -For returned messages, the template's `mandatory` property must be set to `true` or the `mandatory-expression` -must evaluate to `true` for a particular message. -This feature requires a `CachingConnectionFactory` that has its `publisherReturns` property set to `true` (see <>). -Returns are sent to the client by it registering a `RabbitTemplate.ReturnsCallback` by calling `setReturnsCallback(ReturnsCallback callback)`. -The callback must implement the following method: - -==== -[source,java] ----- -void returnedMessage(ReturnedMessage returned); ----- -==== - -The `ReturnedMessage` has the following properties: - -- `message` - the returned message itself -- `replyCode` - a code indicating the reason for the return -- `replyText` - a textual reason for the return - e.g. `NO_ROUTE` -- `exchange` - the exchange to which the message was sent -- `routingKey` - the routing key that was used - -Only one `ReturnsCallback` is supported by each `RabbitTemplate`. -See also <>. - -For publisher confirms (also known as publisher acknowledgements), the template requires a `CachingConnectionFactory` that has its `publisherConfirm` property set to `ConfirmType.CORRELATED`. -Confirms are sent to the client by it registering a `RabbitTemplate.ConfirmCallback` by calling `setConfirmCallback(ConfirmCallback callback)`. -The callback must implement this method: - -==== -[source,java] ----- -void confirm(CorrelationData correlationData, boolean ack, String cause); ----- -==== - -The `CorrelationData` is an object supplied by the client when sending the original message. -The `ack` is true for an `ack` and false for a `nack`. -For `nack` instances, the cause may contain a reason for the `nack`, if it is available when the `nack` is generated. -An example is when sending a message to a non-existent exchange. -In that case, the broker closes the channel. -The reason for the closure is included in the `cause`. -The `cause` was added in version 1.4. - -Only one `ConfirmCallback` is supported by a `RabbitTemplate`. - -NOTE: When a rabbit template send operation completes, the channel is closed. -This precludes the reception of confirms or returns when the connection factory cache is full (when there is space in the cache, the channel is not physically closed and the returns and confirms proceed normally). -When the cache is full, the framework defers the close for up to five seconds, in order to allow time for the confirms and returns to be received. -When using confirms, the channel is closed when the last confirm is received. -When using only returns, the channel remains open for the full five seconds. -We generally recommend setting the connection factory's `channelCacheSize` to a large enough value so that the channel on which a message is published is returned to the cache instead of being closed. -You can monitor channel usage by using the RabbitMQ management plugin. -If you see channels being opened and closed rapidly, you should consider increasing the cache size to reduce overhead on the server. - -IMPORTANT: Before version 2.1, channels enabled for publisher confirms were returned to the cache before the confirms were received. -Some other process could check out the channel and perform some operation that causes the channel to close -- such as publishing a message to a non-existent exchange. -This could cause the confirm to be lost. -Version 2.1 and later no longer return the channel to the cache while confirms are outstanding. -The `RabbitTemplate` performs a logical `close()` on the channel after each operation. -In general, this means that only one confirm is outstanding on a channel at a time. - -NOTE: Starting with version 2.2, the callbacks are invoked on one of the connection factory's `executor` threads. -This is to avoid a potential deadlock if you perform Rabbit operations from within the callback. -With previous versions, the callbacks were invoked directly on the `amqp-client` connection I/O thread; this would deadlock if you perform some RPC operation (such as opening a new channel) since the I/O thread blocks waiting for the result, but the result needs to be processed by the I/O thread itself. -With those versions, it was necessary to hand off work (such as sending a messasge) to another thread within the callback. -This is no longer necessary since the framework now hands off the callback invocation to the executor. - -IMPORTANT: The guarantee of receiving a returned message before the ack is still maintained as long as the return callback executes in 60 seconds or less. -The confirm is scheduled to be delivered after the return callback exits or after 60 seconds, whichever comes first. - -The `CorrelationData` object has a `CompletableFuture` that you can use to get the result, instead of using a `ConfirmCallback` on the template. -The following example shows how to configure a `CorrelationData` instance: - -==== -[source, java] ----- -CorrelationData cd1 = new CorrelationData(); -this.templateWithConfirmsEnabled.convertAndSend("exchange", queue.getName(), "foo", cd1); -assertTrue(cd1.getFuture().get(10, TimeUnit.SECONDS).isAck()); -ReturnedMessage = cd1.getReturn(); -... ----- -==== - -Since it is a `CompletableFuture`, you can either `get()` the result when ready or use `whenComplete()` for an asynchronous callback. -The `Confirm` object is a simple bean with 2 properties: `ack` and `reason` (for `nack` instances). -The reason is not populated for broker-generated `nack` instances. -It is populated for `nack` instances generated by the framework (for example, closing the connection while `ack` instances are outstanding). - -In addition, when both confirms and returns are enabled, the `CorrelationData` `return` property is populated with the returned message, if it couldn't be routed to any queue. -It is guaranteed that the returned message property is set before the future is set with the `ack`. -`CorrelationData.getReturn()` returns a `ReturnMessage` with properties: - -* message (the returned message) -* replyCode -* replyText -* exchange -* routingKey - -See also <> for a simpler mechanism for waiting for publisher confirms. - -[[scoped-operations]] -===== Scoped Operations - -Normally, when using the template, a `Channel` is checked out of the cache (or created), used for the operation, and returned to the cache for reuse. -In a multi-threaded environment, there is no guarantee that the next operation uses the same channel. -There may be times, however, where you want to have more control over the use of a channel and ensure that a number of operations are all performed on the same channel. - -Starting with version 2.0, a new method called `invoke` is provided, with an `OperationsCallback`. -Any operations performed within the scope of the callback and on the provided `RabbitOperations` argument use the same dedicated `Channel`, which will be closed at the end (not returned to a cache). -If the channel is a `PublisherCallbackChannel`, it is returned to the cache after all confirms have been received (see <>). - -==== -[source, java] ----- -@FunctionalInterface -public interface OperationsCallback { - - T doInRabbit(RabbitOperations operations); - -} ----- -==== - -One example of why you might need this is if you wish to use the `waitForConfirms()` method on the underlying `Channel`. -This method was not previously exposed by the Spring API because the channel is, generally, cached and shared, as discussed earlier. -The `RabbitTemplate` now provides `waitForConfirms(long timeout)` and `waitForConfirmsOrDie(long timeout)`, which delegate to the dedicated channel used within the scope of the `OperationsCallback`. -The methods cannot be used outside of that scope, for obvious reasons. - -Note that a higher-level abstraction that lets you correlate confirms to requests is provided elsewhere (see <>). -If you want only to wait until the broker has confirmed delivery, you can use the technique shown in the following example: - -==== -[source, java] ----- -Collection messages = getMessagesToSend(); -Boolean result = this.template.invoke(t -> { - messages.forEach(m -> t.convertAndSend(ROUTE, m)); - t.waitForConfirmsOrDie(10_000); - return true; -}); ----- -==== - -If you wish `RabbitAdmin` operations to be invoked on the same channel within the scope of the `OperationsCallback`, the admin must have been constructed by using the same `RabbitTemplate` that was used for the `invoke` operation. - -NOTE: The preceding discussion is moot if the template operations are already performed within the scope of an existing transaction -- for example, when running on a transacted listener container thread and performing operations on a transacted template. -In that case, the operations are performed on that channel and committed when the thread returns to the container. -It is not necessary to use `invoke` in that scenario. - -When using confirms in this way, much of the infrastructure set up for correlating confirms to requests is not really needed (unless returns are also enabled). -Starting with version 2.2, the connection factory supports a new property called `publisherConfirmType`. -When this is set to `ConfirmType.SIMPLE`, the infrastructure is avoided and the confirm processing can be more efficient. - -Furthermore, the `RabbitTemplate` sets the `publisherSequenceNumber` property in the sent message `MessageProperties`. -If you wish to check (or log or otherwise use) specific confirms, you can do so with an overloaded `invoke` method, as the following example shows: - -==== -[source, java] ----- -public T invoke(OperationsCallback action, com.rabbitmq.client.ConfirmCallback acks, - com.rabbitmq.client.ConfirmCallback nacks); ----- -==== - -NOTE: These `ConfirmCallback` objects (for `ack` and `nack` instances) are the Rabbit client callbacks, not the template callback. - -The following example logs `ack` and `nack` instances: - -==== -[source, java] ----- -Collection messages = getMessagesToSend(); -Boolean result = this.template.invoke(t -> { - messages.forEach(m -> t.convertAndSend(ROUTE, m)); - t.waitForConfirmsOrDie(10_000); - return true; -}, (tag, multiple) -> { - log.info("Ack: " + tag + ":" + multiple); -}, (tag, multiple) -> { - log.info("Nack: " + tag + ":" + multiple); -})); ----- -==== - -IMPORTANT: Scoped operations are bound to a thread. -See <> for a discussion about strict ordering in a multi-threaded environment. - -[[multi-strict]] -===== Strict Message Ordering in a Multi-Threaded Environment - -The discussion in <> applies only when the operations are performed on the same thread. - -Consider the following situation: - -* `thread-1` sends a message to a queue and hands off work to `thread-2` -* `thread-2` sends a message to the same queue - -Because of the async nature of RabbitMQ and the use of cached channels; it is not certain that the same channel will be used and therefore the order in which the messages arrive in the queue is not guaranteed. -(In most cases they will arrive in order, but the probability of out-of-order delivery is not zero). -To solve this use case, you can use a bounded channel cache with size `1` (together with a `channelCheckoutTimeout`) to ensure the messages are always published on the same channel, and order will be guaranteed. -To do this, if you have other uses for the connection factory, such as consumers, you should either use a dedicated connection factory for the template, or configure the template to use the publisher connection factory embedded in the main connection factory (see <>). - -This is best illustrated with a simple Spring Boot Application: - -==== -[source, java] ----- -@SpringBootApplication -public class Application { - - private static final Logger log = LoggerFactory.getLogger(Application.class); - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } - - @Bean - TaskExecutor exec() { - ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor(); - exec.setCorePoolSize(10); - return exec; - } - - @Bean - CachingConnectionFactory ccf() { - CachingConnectionFactory ccf = new CachingConnectionFactory("localhost"); - CachingConnectionFactory publisherCF = (CachingConnectionFactory) ccf.getPublisherConnectionFactory(); - publisherCF.setChannelCacheSize(1); - publisherCF.setChannelCheckoutTimeout(1000L); - return ccf; - } - - @RabbitListener(queues = "queue") - void listen(String in) { - log.info(in); - } - - @Bean - Queue queue() { - return new Queue("queue"); - } - - - @Bean - public ApplicationRunner runner(Service service, TaskExecutor exec) { - return args -> { - exec.execute(() -> service.mainService("test")); - }; - } - -} - -@Component -class Service { - - private static final Logger LOG = LoggerFactory.getLogger(Service.class); - - private final RabbitTemplate template; - - private final TaskExecutor exec; - - Service(RabbitTemplate template, TaskExecutor exec) { - template.setUsePublisherConnection(true); - this.template = template; - this.exec = exec; - } - - void mainService(String toSend) { - LOG.info("Publishing from main service"); - this.template.convertAndSend("queue", toSend); - this.exec.execute(() -> secondaryService(toSend.toUpperCase())); - } - - void secondaryService(String toSend) { - LOG.info("Publishing from secondary service"); - this.template.convertAndSend("queue", toSend); - } - -} ----- -==== - -Even though the publishing is performed on two different threads, they will both use the same channel because the cache is capped at a single channel. - -Starting with version 2.3.7, the `ThreadChannelConnectionFactory` supports transferring a thread's channel(s) to another thread, using the `prepareContextSwitch` and `switchContext` methods. -The first method returns a context which is passed to the second thread which calls the second method. -A thread can have either a non-transactional channel or a transactional channel (or one of each) bound to it; you cannot transfer them individually, unless you use two connection factories. -An example follows: - -==== -[source, java] ----- -@SpringBootApplication -public class Application { - - private static final Logger log = LoggerFactory.getLogger(Application.class); - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } - - @Bean - TaskExecutor exec() { - ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor(); - exec.setCorePoolSize(10); - return exec; - } - - @Bean - ThreadChannelConnectionFactory tccf() { - ConnectionFactory rabbitConnectionFactory = new ConnectionFactory(); - rabbitConnectionFactory.setHost("localhost"); - return new ThreadChannelConnectionFactory(rabbitConnectionFactory); - } - - @RabbitListener(queues = "queue") - void listen(String in) { - log.info(in); - } - - @Bean - Queue queue() { - return new Queue("queue"); - } - - - @Bean - public ApplicationRunner runner(Service service, TaskExecutor exec) { - return args -> { - exec.execute(() -> service.mainService("test")); - }; - } - -} - -@Component -class Service { - - private static final Logger LOG = LoggerFactory.getLogger(Service.class); - - private final RabbitTemplate template; - - private final TaskExecutor exec; - - private final ThreadChannelConnectionFactory connFactory; - - Service(RabbitTemplate template, TaskExecutor exec, - ThreadChannelConnectionFactory tccf) { - - this.template = template; - this.exec = exec; - this.connFactory = tccf; - } - - void mainService(String toSend) { - LOG.info("Publishing from main service"); - this.template.convertAndSend("queue", toSend); - Object context = this.connFactory.prepareSwitchContext(); - this.exec.execute(() -> secondaryService(toSend.toUpperCase(), context)); - } - - void secondaryService(String toSend, Object threadContext) { - LOG.info("Publishing from secondary service"); - this.connFactory.switchContext(threadContext); - this.template.convertAndSend("queue", toSend); - this.connFactory.closeThreadChannel(); - } - -} ----- -==== - -IMPORTANT: Once the `prepareSwitchContext` is called, if the current thread performs any more operations, they will be performed on a new channel. -It is important to close the thread-bound channel when it is no longer needed. - -[[template-messaging]] -===== Messaging Integration - -Starting with version 1.4, `RabbitMessagingTemplate` (built on top of `RabbitTemplate`) provides an integration with the Spring Framework messaging abstraction -- that is, -`org.springframework.messaging.Message`. -This lets you send and receive messages by using the `spring-messaging` `Message` abstraction. -This abstraction is used by other Spring projects, such as Spring Integration and Spring's STOMP support. -There are two message converters involved: one to convert between a spring-messaging `Message` and Spring AMQP's `Message` abstraction and one to convert between Spring AMQP's `Message` abstraction and the format required by the underlying RabbitMQ client library. -By default, the message payload is converted by the provided `RabbitTemplate` instance's message converter. -Alternatively, you can inject a custom `MessagingMessageConverter` with some other payload converter, as the following example shows: - -==== -[source, java] ----- -MessagingMessageConverter amqpMessageConverter = new MessagingMessageConverter(); -amqpMessageConverter.setPayloadConverter(myPayloadConverter); -rabbitMessagingTemplate.setAmqpMessageConverter(amqpMessageConverter); ----- -==== - -[[template-user-id]] -===== Validated User Id - -Starting with version 1.6, the template now supports a `user-id-expression` (`userIdExpression` when using Java configuration). -If a message is sent, the user id property is set (if not already set) after evaluating this expression. -The root object for the evaluation is the message to be sent. - -The following examples show how to use the `user-id-expression` attribute: - -==== -[source, xml] ----- - - - ----- -==== - -The first example is a literal expression. -The second obtains the `username` property from a connection factory bean in the application context. - -[[separate-connection]] -===== Using a Separate Connection - -Starting with version 2.0.2, you can set the `usePublisherConnection` property to `true` to use a different connection to that used by listener containers, when possible. -This is to avoid consumers being blocked when a producer is blocked for any reason. -The connection factories maintain a second internal connection factory for this purpose; by default it is the same type as the main factory, but can be set explicitly if you wish to use a different factory type for publishing. -If the rabbit template is running in a transaction started by the listener container, the container's channel is used, regardless of this setting. - -IMPORTANT: In general, you should not use a `RabbitAdmin` with a template that has this set to `true`. -Use the `RabbitAdmin` constructor that takes a connection factory. -If you use the other constructor that takes a template, ensure the template's property is `false`. -This is because, often, an admin is used to declare queues for listener containers. -Using a template that has the property set to `true` would mean that exclusive queues (such as `AnonymousQueue`) would be declared on a different connection to that used by listener containers. -In that case, the queues cannot be used by the containers. - -[[sending-messages]] -==== Sending Messages - -When sending a message, you can use any of the following methods: - -==== -[source,java] ----- -void send(Message message) throws AmqpException; - -void send(String routingKey, Message message) throws AmqpException; - -void send(String exchange, String routingKey, Message message) throws AmqpException; ----- -==== - -We can begin our discussion with the last method in the preceding listing, since it is actually the most explicit. -It lets an AMQP exchange name (along with a routing key)be provided at runtime. -The last parameter is the callback that is responsible for actual creating the message instance. -An example of using this method to send a message might look like this: -The following example shows how to use the `send` method to send a message: - -==== -[source,java] ----- -amqpTemplate.send("marketData.topic", "quotes.nasdaq.THING1", - new Message("12.34".getBytes(), someProperties)); ----- -==== - -You can set the `exchange` property on the template itself if you plan to use that template instance to send to the same exchange most or all of the time. -In such cases, you can use the second method in the preceding listing. -The following example is functionally equivalent to the previous example: - -==== -[source,java] ----- -amqpTemplate.setExchange("marketData.topic"); -amqpTemplate.send("quotes.nasdaq.FOO", new Message("12.34".getBytes(), someProperties)); ----- -==== - -If both the `exchange` and `routingKey` properties are set on the template, you can use the method that accepts only the `Message`. -The following example shows how to do so: - -==== -[source,java] ----- -amqpTemplate.setExchange("marketData.topic"); -amqpTemplate.setRoutingKey("quotes.nasdaq.FOO"); -amqpTemplate.send(new Message("12.34".getBytes(), someProperties)); ----- -==== - -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. -In both cases, the default is an empty `String`, but that is actually a sensible default. -As far as the routing key is concerned, it is not always necessary in the first place (for example, for -a `Fanout` exchange). -Furthermore, a queue may be bound to an exchange with an empty `String`. -Those are both legitimate scenarios for reliance on the default empty `String` value for the routing key property of the template. -As far as the exchange name is concerned, the empty `String` is commonly used because the AMQP specification defines the "`default exchange`" as having no name. -Since all queues are automatically bound to that default exchange (which is a direct exchange), using their name as the binding value, the second method in the preceding listing can be used for simple point-to-point messaging to any queue through the default exchange. -You can provide the queue name as the `routingKey`, either by providing the method parameter at runtime. -The following example shows how to do so: - -==== -[source,java] ----- -RabbitTemplate template = new RabbitTemplate(); // using default no-name Exchange -template.send("queue.helloWorld", new Message("Hello World".getBytes(), someProperties)); ----- -==== - -Alternately, you can create a template that can be used for publishing primarily or exclusively to a single Queue. -The following example shows how to do so: - -==== -[source,java] ----- -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]] -===== Message Builder API - -Starting with version 1.3, a message builder API is provided by the `MessageBuilder` and `MessagePropertiesBuilder`. -These methods provide a convenient "`fluent`" means of creating a message or message properties. -The following examples show the fluent API in action: - -==== -[source,java] ----- -Message message = MessageBuilder.withBody("foo".getBytes()) - .setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN) - .setMessageId("123") - .setHeader("bar", "baz") - .build(); ----- - -[source,java] ----- -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(); ----- -==== - -Each of the properties defined on the https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/core/MessageProperties.html[`MessageProperties`] can be set. -Other methods include `setHeader(String key, String value)`, `removeHeader(String key)`, `removeHeaders()`, and `copyProperties(MessageProperties properties)`. -Each property setting method has a `set*IfAbsent()` variant. -In the cases where a default initial value exists, the method is named `set*IfAbsentOrDefault()`. - -Five static methods are provided to create an initial message builder: - -==== -[source,java] ----- -public static MessageBuilder withBody(byte[] body) <1> - -public static MessageBuilder withClonedBody(byte[] body) <2> - -public static MessageBuilder withBody(byte[] body, int from, int to) <3> - -public static MessageBuilder fromMessage(Message message) <4> - -public static MessageBuilder fromClonedMessage(Message message) <5> ----- - -<1> The message created by the builder has a body that is a direct reference to the argument. -<2> The message created by the builder has a body that is a new array containing a copy of bytes in the argument. -<3> The message created by the builder has a body that is a new array containing the range of bytes from the argument. -See https://docs.oracle.com/javase/7/docs/api/java/util/Arrays.html[`Arrays.copyOfRange()`] for more details. -<4> The message created by the builder has a body that is a direct reference to the body of the argument. -The argument's properties are copied to a new `MessageProperties` object. -<5> The message created by the builder has a body that is a new array containing a copy of the argument's body. -The argument's properties are copied to a new `MessageProperties` object. -==== - -Three static methods are provided to create a `MessagePropertiesBuilder` instance: - -==== -[source,java] ----- -public static MessagePropertiesBuilder newInstance() <1> - -public static MessagePropertiesBuilder fromProperties(MessageProperties properties) <2> - -public static MessagePropertiesBuilder fromClonedProperties(MessageProperties properties) <3> ----- - -<1> A new message properties object is initialized with default values. -<2> The builder is initialized with, and `build()` will return, the provided properties object., -<3> The argument's properties are copied to a new `MessageProperties` object. -==== - -With the `RabbitTemplate` implementation of `AmqpTemplate`, each of the `send()` methods has an overloaded version that takes an additional `CorrelationData` object. -When publisher confirms are enabled, this object is returned in the callback described in <>. -This lets the sender correlate a confirm (`ack` or `nack`) with the sent message. - -Starting with version 1.6.7, the `CorrelationAwareMessagePostProcessor` interface was introduced, allowing the correlation data to be modified after the message has been converted. -The following example shows how to use it: - -==== -[source, java] ----- -Message postProcessMessage(Message message, Correlation correlation); ----- -==== - -In version 2.0, this interface is deprecated. -The method has been moved to `MessagePostProcessor` with a default implementation that delegates to `postProcessMessage(Message message)`. - -Also starting with version 1.6.7, a new callback interface called `CorrelationDataPostProcessor` is provided. -This is invoked after all `MessagePostProcessor` instances (provided in the `send()` method as well as those provided in `setBeforePublishPostProcessors()`). -Implementations can update or replace the correlation data supplied in the `send()` method (if any). -The `Message` and original `CorrelationData` (if any) are provided as arguments. -The following example shows how to use the `postProcess` method: - -==== -[source, java] ----- -CorrelationData postProcess(Message message, CorrelationData correlationData); ----- -==== - -===== Publisher Returns - -When the template's `mandatory` property is `true`, returned messages are provided by the callback described in <>. - -Starting with version 1.4, the `RabbitTemplate` supports the SpEL `mandatoryExpression` property, which is evaluated against each request message as the root evaluation object, resolving to a `boolean` value. -Bean references, such as `@myBean.isMandatory(#root)`, can be used in the expression. - -Publisher returns can also be used internally by the `RabbitTemplate` in send and receive operations. -See <> for more information. - -[[template-batching]] -===== Batching - -Version 1.4.2 introduced the `BatchingRabbitTemplate`. -This is a subclass of `RabbitTemplate` with an overridden `send` method that batches messages according to the `BatchingStrategy`. -Only when a batch is complete is the message sent to RabbitMQ. -The following listing shows the `BatchingStrategy` interface definition: - -==== -[source, java] ----- -public interface BatchingStrategy { - - MessageBatch addToBatch(String exchange, String routingKey, Message message); - - Date nextRelease(); - - Collection releaseBatches(); - -} ----- -==== - -CAUTION: Batched data is held in memory. -Unsent messages can be lost in the event of a system failure. - -A `SimpleBatchingStrategy` is provided. -It supports sending messages to a single exchange or routing key. -It has the following properties: - -* `batchSize`: The number of messages in a batch before it is sent. -* `bufferLimit`: The maximum size of the batched message. -This preempts the `batchSize`, if exceeded, and causes a partial batch to be sent. -* `timeout`: A time after which a partial batch is sent when there is no new activity adding messages to the batch. - -The `SimpleBatchingStrategy` formats the batch by preceding each embedded message with a four-byte binary length. -This is communicated to the receiving system by setting the `springBatchFormat` message property to `lengthHeader4`. - -IMPORTANT: 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. - -However, see <> for more information. - -[[receiving-messages]] -==== 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. -We consider an example of each approach in the next two sub-sections. - -[[polling-consumer]] -===== 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. -Starting with version 1.5, 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. - -CAUTION: 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)`. - -There are four simple `receive` methods available. -As with the `Exchange` on the sending side, there is a method that requires that a default queue property has been set -directly on the template itself, and there is a method that accepts a queue parameter at runtime. -Version 1.6 introduced variants to accept `timeoutMillis` to override `receiveTimeout` on a per-request basis. -The following listing shows the definitions of the four methods: - -==== -[source,java] ----- -Message receive() throws AmqpException; - -Message receive(String queueName) throws AmqpException; - -Message receive(long timeoutMillis) throws AmqpException; - -Message receive(String queueName, long timeoutMillis) throws AmqpException; ----- -==== - -As in the case of sending messages, the `AmqpTemplate` has some convenience methods for receiving POJOs instead of `Message` instances, and implementations provide a way to customize the `MessageConverter` used to create the `Object` returned: -The following listing shows those methods: - -==== -[source,java] ----- -Object receiveAndConvert() throws AmqpException; - -Object receiveAndConvert(String queueName) throws AmqpException; - -Object receiveAndConvert(long timeoutMillis) throws AmqpException; - -Object receiveAndConvert(String queueName, long timeoutMillis) throws AmqpException; ----- -==== - -Starting with version 2.0, there are variants of these methods that take an additional `ParameterizedTypeReference` argument to convert complex types. -The template must be configured with a `SmartMessageConverter`. -See <> for more information. - -Similar to `sendAndReceive` methods, beginning with version 1.3, the `AmqpTemplate` has several convenience `receiveAndReply` methods for synchronously receiving, processing and replying to messages. -The following listing shows those method definitions: - -==== -[source,java] ----- - boolean receiveAndReply(ReceiveAndReplyCallback callback) - throws AmqpException; - - boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback) - throws AmqpException; - - boolean receiveAndReply(ReceiveAndReplyCallback callback, - String replyExchange, String replyRoutingKey) throws AmqpException; - - boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback, - String replyExchange, String replyRoutingKey) throws AmqpException; - - boolean receiveAndReply(ReceiveAndReplyCallback callback, - ReplyToAddressCallback replyToAddressCallback) throws AmqpException; - - boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback, - ReplyToAddressCallback replyToAddressCallback) throws AmqpException; ----- -==== - -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. - -Automatic message (request and reply) conversion is applied only if the provided callback is not an instance of `ReceiveAndReplyMessageCallback`, which provides a raw message exchange contract. - -The `ReplyToAddressCallback` is useful for cases requiring custom logic to determine the `replyTo` address at runtime against the received message and reply from the `ReceiveAndReplyCallback`. -By default, `replyTo` information in the request message is used to route the reply. - -The following listing shows an example of POJO-based receive and reply: - -==== -[source,java] ----- -boolean received = - this.template.receiveAndReply(ROUTE, new ReceiveAndReplyCallback() { - - public Invoice handle(Order order) { - return processOrder(order); - } - }); -if (received) { - log.info("We received an order!"); -} ----- -==== - -[[async-consumer]] -===== Asynchronous Consumer - -IMPORTANT: 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. -See <> for more details. - -[IMPORTANT] -==== -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. - -See <>. - -For more background about prefetch, see this post about https://www.rabbitmq.com/blog/2014/04/14/finding-bottlenecks-with-rabbitmq-3-3/[consumer utilization in RabbitMQ] -and this post about https://www.rabbitmq.com/blog/2012/05/11/some-queuing-theory-throughput-latency-and-bandwidth/[queuing theory]. -==== - -====== Message Listener - -For asynchronous `Message` reception, a dedicated component (not the `AmqpTemplate`) is involved. -That component is a container for a `Message`-consuming callback. -We consider the container and its properties later in this section. -First, though, we should look at the callback, since that is where your application code is integrated with the messaging system. -There are a few options for the callback, starting with an implementation of the `MessageListener` interface, which the following listing shows: - -==== -[source,java] ----- -public interface MessageListener { - void onMessage(Message message); -} ----- -==== - -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. -The following listing shows the `ChannelAwareMessageListener` interface definition: - -==== -[source,java] ----- -public interface ChannelAwareMessageListener { - void onMessage(Message message, Channel channel) throws Exception; -} ----- -==== - -IMPORTANT: In version 2.1, this interface moved from package `o.s.amqp.rabbit.core` to `o.s.amqp.rabbit.listener.api`. - -[[message-listener-adapter]] -====== `MessageListenerAdapter` - -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. - -NOTE: Version 1.5 introduced a more flexible mechanism for POJO messaging, the `@RabbitListener` annotation. -See <> for more information. - -When using the adapter, you need to provide only a reference to the instance that the adapter itself should invoke. -The following example shows how to do so: - -==== -[source,java] ----- -MessageListenerAdapter listener = new MessageListenerAdapter(somePojo); -listener.setDefaultListenerMethod("myMethod"); ----- -==== - -You can subclass the adapter and provide an implementation of `getListenerMethodName()` to dynamically select different methods based on the message. -This method has two parameters, `originalMessage` and `extractedMessage`, the latter being the result of any conversion. -By default, a `SimpleMessageConverter` is configured. -See <> for more information and information about other converters available. - -Starting with version 1.4.2, the original message has `consumerQueue` and `consumerTag` properties, which can be used to determine the queue from which a message was received. - -Starting with version 1.5, you can configure a map of consumer queue or tag to method name, to dynamically select the method to call. -If no entry is in the map, we fall back to the default listener method. -The default listener method (if not set) is `handleMessage`. - -Starting with version 2.0, a convenient `FunctionalInterface` has been provided. -The following listing shows the definition of `FunctionalInterface`: - -==== -[source, java] ----- -@FunctionalInterface -public interface ReplyingMessageListener { - - R handleMessage(T t); - -} ----- -==== - -This interface facilitates convenient configuration of the adapter by using Java 8 lambdas, as the following example shows: - -==== -[source, java] ----- -new MessageListenerAdapter((ReplyingMessageListener) data -> { - ... - return result; -})); ----- -==== - -Starting with version 2.2, the `buildListenerArguments(Object)` has been deprecated and new `buildListenerArguments(Object, Channel, Message)` one has been introduced instead. -The new method helps listener to get `Channel` and `Message` arguments to do more, such as calling `channel.basicReject(long, boolean)` in manual acknowledge mode. -The following listing shows the most basic example: - -==== -[source,java] ----- -public class ExtendedListenerAdapter extends MessageListenerAdapter { - - @Override - protected Object[] buildListenerArguments(Object extractedMessage, Channel channel, Message message) { - return new Object[]{extractedMessage, channel, message}; - } - -} ----- -==== - -Now you could configure `ExtendedListenerAdapter` as same as `MessageListenerAdapter` if you need to receive "`channel`" and "`message`". -Parameters of listener should be set as `buildListenerArguments(Object, Channel, Message)` returned, as the following example of listener shows: - -==== -[source,java] ----- -public void handleMessage(Object object, Channel channel, Message message) throws IOException { - ... -} ----- -==== - -====== 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. - -Prior to version 2.0, there was one listener container, the `SimpleMessageListenerContainer`. -There is now a second container, the `DirectMessageListenerContainer`. -The differences between the containers and criteria you might apply when choosing which to use are described in <>. - -The following listing shows the most basic example, which works by using the, `SimpleMessageListenerContainer`: - -==== -[source,java] ----- -SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); -container.setConnectionFactory(rabbitConnectionFactory); -container.setQueueNames("some.queue"); -container.setMessageListener(new MessageListenerAdapter(somePojo)); ----- -==== - -As an "`active`" component, it is most common to create the listener container with a bean definition so that it can run in the background. -The following example shows one way to do so with XML: - -==== -[source,xml] ----- - - - ----- -==== - -The following listing shows another way to do so with XML: - -==== -[source,xml] ----- - - - ----- -==== - -Both of the preceding examples create a `DirectMessageListenerContainer` (notice the `type` attribute -- it defaults to `simple`). - -Alternately, you may prefer to use Java configuration, which looks similar to the preceding code snippet: - -==== -[source,java] ----- -@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]] -====== Consumer Priority - -Starting with RabbitMQ Version 3.2, the broker now supports consumer priority (see https://www.rabbitmq.com/blog/2013/12/16/using-consumer-priorities-with-rabbitmq/[Using Consumer Priorities with RabbitMQ]). -This is enabled by setting the `x-priority` argument on the consumer. -The `SimpleMessageListenerContainer` now supports setting consumer arguments, as the following example shows: - -==== -[source,java] ----- - -container.setConsumerArguments(Collections. - singletonMap("x-priority", Integer.valueOf(10))); ----- -==== - -For convenience, the namespace provides the `priority` attribute on the `listener` element, as the following example shows: - -==== -[source,xml] ----- - - - ----- -==== - -Starting with version 1.3, you can modify the queues on which the container listens at runtime. -See <>. - -[[lc-auto-delete]] -====== `auto-delete` Queues - -When a container is configured to listen to `auto-delete` queues, the queue has an `x-expires` option, or the https://www.rabbitmq.com/ttl.html[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). -Before version 1.3, the container could not be restarted because the queue was missing. -The `RabbitAdmin` only automatically redeclares queues and so on when the connection is closed or when it opens, which does not happen when the container is stopped and started. - -Starting with version 1.3, the container uses a `RabbitAdmin` to redeclare any missing queues during startup. - -You can also use conditional declaration (see <>) together with an `auto-startup="false"` admin to defer queue declaration until the container is started. -The following example shows how to do so: - -==== -[source,xml] ----- - - - - - - - - - - - - - ----- -==== - -In this case, 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. - -[[de-batching]] -===== 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. -See <> for more information about batching. - -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 <> as `List`. -See <> for information about using this feature with `@RabbitListener`. - -[[consumer-events]] -===== Consumer Events - -The containers publish application events whenever a listener -(consumer) experiences a failure of some kind. -The event `ListenerContainerConsumerFailedEvent` has the following properties: - -* `container`: The listener container where the consumer experienced the problem. -* `reason`: A textual reason for the failure. -* `fatal`: A boolean indicating whether the failure was fatal. -With non-fatal exceptions, the container tries to restart the consumer, according to the `recoveryInterval` or `recoveryBackoff` (for the `SimpleMessageListenerContainer`) or the `monitorInterval` (for the `DirectMessageListenerContainer`). -* `throwable`: The `Throwable` that was caught. - -These events can be consumed by implementing `ApplicationListener`. - -NOTE: System-wide events (such as connection failures) are published by all consumers when `concurrentConsumers` is greater than 1. - -If a consumer fails because one if its queues is being used exclusively, by default, as well as publishing the event, a `DEBUG` log is issued (since 3.1, previously WARN). -To change this logging behavior, provide a custom `ConditionalExceptionLogger` in the `AbstractMessageListenerContainer` instance's `exclusiveConsumerExceptionLogger` property. -In addition, the `SimpleMessageListenerContainer` consumer restart after such an exception is now logged at DEBUG level by default (previously INFO). -A new method `logRestart()` has been added to the `ConditionalExceptionLogger` to allow this to be changed. - -Also, the `AbstractMessageListenerContainer.DefaultExclusiveConsumerLogger` is now public, allowing it to be sub classed. - -See also <>. - -Fatal errors are always logged at the `ERROR` level. -This it not modifiable. - -Several other events are published at various stages of the container lifecycle: - -* `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 a `consumeOk` is received from the broker, contains the queue name and `consumerTag` -* `ListenerContainerIdleEvent`: See <>. -* `MissingQueueEvent`: When a missing queue is detected. - -[[consumerTags]] -===== Consumer Tags - -You can provide a strategy to generate consumer tags. -By default, the consumer tag is generated by the broker. -The following listing shows the `ConsumerTagStrategy` interface definition: - -==== -[source,java] ----- -public interface ConsumerTagStrategy { - - String createConsumerTag(String queue); - -} ----- -==== - -The queue is made available so that it can (optionally) be used in the tag. - -See <>. - -[[async-annotation-driven]] -===== Annotation-driven Listener Endpoints - -The easiest way to receive a message asynchronously is to use the annotated listener endpoint infrastructure. -In a nutshell, it lets you expose a method of a managed bean as a Rabbit listener endpoint. -The following example shows how to use the `@RabbitListener` annotation: - -==== -[source,java] ----- - -@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. - -NOTE: Property placeholders (`${some.property}`) or SpEL expressions (`#{someExpression}`) can be specified for the annotation properties (`queues` etc). -See <> for an example of why you might use SpEL instead of a property placeholder. -The following listing shows three examples of how to declare a Rabbit listener: - -==== -[source,java] ----- - -@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(Order order) { - ... - } - - @RabbitListener(bindings = @QueueBinding( - value = @Queue, - exchange = @Exchange(value = "auto.exch"), - key = "invoiceRoutingKey") - ) - public void processInvoice(Invoice invoice) { - ... - } - - @RabbitListener(queuesToDeclare = @Queue(name = "${my.queue}", durable = "true")) - public String handleWithSimpleDeclare(String data) { - ... - } - -} ----- -==== - -In the first example, a queue `myQueue` is declared automatically (durable) together with the exchange, if needed, -and bound to the exchange with the routing key. -In the second example, 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; see <>. -Multiple `QueueBinding` entries can be provided, letting the listener listen to multiple queues. -In the third example, 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. - -Since version 2.0, the `@Exchange` annotation supports any exchange types, including custom. -For more information, see https://www.rabbitmq.com/tutorials/amqp-concepts.html[AMQP Concepts]. - -You can use normal `@Bean` definitions when you need more advanced configuration. - -Notice `ignoreDeclarationExceptions` on the exchange in the first example. -This allows, for example, binding to an existing exchange that might have different settings (such as `internal`). -By default, the properties of an existing exchange must match. - -Starting with version 2.0, you can now bind a queue to an exchange with multiple routing keys, as the following example shows: - -==== -[source, java] ----- -... - key = { "red", "yellow" } -... ----- -==== - -You can also specify arguments within `@QueueBinding` annotations for queues, exchanges, -and bindings, as the following example shows: - -==== -[source, java] ----- -@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 = "thing1", value = "somevalue"), - @Argument(name = "thing2") - }) -) -public String handleWithHeadersExchange(String foo) { - ... -} ----- -==== - -Notice that the `x-message-ttl` argument is set to 10 seconds for the queue. -Since the argument type is not `String`, we have to specify its type -- in this case, `Integer`. -As with all such declarations, if the queue already exists, the arguments must match those on the queue. -For the header exchange, we set the binding arguments to match messages that have the `thing1` header set to `somevalue`, and -the `thing2` header must be present with any value. -The `x-match` argument means both conditions must be satisfied. - -The argument name, value, and type can be property placeholders (`${...}`) or SpEL expressions (`#{...}`). -The `name` must resolve to a `String`. -The expression for `type` must resolve to a `Class` or the fully-qualified name of a class. -The `value` must resolve to something that can be converted by the `DefaultConversionService` to the type (such as the `x-message-ttl` in the preceding example). - -If a name resolves to `null` or an empty `String`, that `@Argument` is ignored. - -[[meta-annotation-driven]] -====== 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. -The following example shows how to do so: - -==== -[source, java] ----- -@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) { - ... - } - -} ----- -==== - -In the preceding example, each listener created by the `@MyAnonFanoutListener` annotation binds an anonymous, auto-delete -queue to the fanout exchange, `metaFanout`. -Starting with version 2.2.3, `@AliasFor` is supported to allow overriding properties on the meta-annotated annotation. -Also, user annotations can now be `@Repeatable`, allowing multiple containers to be created for a method. - -==== -[source, java] ----- -@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(); - -} ----- -==== - - -[[async-annotation-driven-enable]] -====== Enable Listener Endpoint Annotations - -To enable support for `@RabbitListener` annotations, you can add `@EnableRabbit` to one of your `@Configuration` classes. -The following example shows how to do so: - -==== -[source,java] ----- -@Configuration -@EnableRabbit -public class AppConfig { - - @Bean - public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() { - SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); - factory.setConnectionFactory(connectionFactory()); - factory.setConcurrentConsumers(3); - factory.setMaxConcurrentConsumers(10); - factory.setContainerCustomizer(container -> /* customize the container */); - return factory; - } -} ----- -==== - -Since version 2.0, a `DirectMessageListenerContainerFactory` is also available. -It creates `DirectMessageListenerContainer` instances. - -NOTE: For information to help you choose between `SimpleRabbitListenerContainerFactory` and `DirectRabbitListenerContainerFactory`, see <>. - -Starting with version 2.2.2, you can provide a `ContainerCustomizer` implementation (as shown above). -This can be used to further configure the container after it has been created and configured; you can use this, for example, to set properties that are not exposed by the container factory. - -Version 2.4.8 provides the `CompositeContainerCustomizer` for situations where you wish to apply multiple customizers. - -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. - -You can customize the listener container factory to use for each annotation, or you can configure an explicit default by implementing the `RabbitListenerConfigurer` interface. -The default is required only if at least one endpoint is registered without a specific container factory. -See the https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/rabbit/annotation/RabbitListenerConfigurer.html[Javadoc] for full details and examples. - -The container factories provide methods for adding `MessagePostProcessor` instances that are applied after receiving messages (before invoking the listener) and before sending replies. - -See <> for information about replies. - -Starting with version 2.0.6, 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. -You can use a `SendRetryContextAccessor` to get information from the context. -The following example shows how to do so: - -==== -[source, java] ----- -factory.setRetryTemplate(retryTemplate); -factory.setReplyRecoveryCallback(ctx -> { - Message failed = SendRetryContextAccessor.getMessage(ctx); - Address replyTo = SendRetryContextAccessor.getAddress(ctx); - Throwable t = ctx.getLastThrowable(); - ... - return null; -}); ----- -==== - -If you prefer XML configuration, you can use the `` element. -Any beans annotated with `@RabbitListener` are detected. - -For `SimpleRabbitListenerContainer` instances, you can use XML similar to the following: - -==== -[source,xml] ----- - - - - - - - ----- -==== - -For `DirectMessageListenerContainer` instances, you can use XML similar to the following: - -==== -[source,xml] ----- - - - - - - ----- -==== - -[[listener-property-overrides]] - -Starting with version 2.0, the `@RabbitListener` annotation has a `concurrency` property. -It supports SpEL expressions (`#{...}`) and property placeholders (`${...}`). -Its meaning and allowed values depend on the container type, as follows: - -* For the `DirectMessageListenerContainer`, the value must be a single integer value, which sets the `consumersPerQueue` property on the container. -* For the `SimpleRabbitListenerContainer`, the value can be a single integer value, which sets the `concurrentConsumers` property on the container, or it can have the form, `m-n`, where `m` is the `concurrentConsumers` property and `n` is the `maxConcurrentConsumers` property. - -In either case, this setting overrides the settings on the factory. -Previously you had to define different container factories if you had listeners that required different concurrency. - -The annotation also allows overriding the factory `autoStartup` and `taskExecutor` properties via the `autoStartup` and `executor` (since 2.2) annotation properties. -Using a different executor for each might help with identifying threads associated with each listener in logs and thread dumps. - -Version 2.2 also added the `ackMode` property, which allows you to override the container factory's `acknowledgeMode` property. - -==== -[source, java] ----- -@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); -} ----- -==== - -[[async-annotation-conversion]] -====== Message Conversion for Annotated Methods - -There are two conversion steps in the pipeline before invoking the listener. -The first step uses a `MessageConverter` to convert the incoming Spring AMQP `Message` to a Spring-messaging `Message`. -When the target method is invoked, the message payload is converted, if necessary, to the method parameter type. - -The default `MessageConverter` for the first step is a Spring AMQP `SimpleMessageConverter` that handles conversion to -`String` and `java.io.Serializable` objects. -All others remain as a `byte[]`. -In the following discussion, we call this the "`message converter`". - -The default converter for the second step is a `GenericMessageConverter`, which delegates to a conversion service -(an instance of `DefaultFormattingConversionService`). -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. -The following example shows how to do so: - -==== -[source, java] ----- -@Bean -public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() { - SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); - ... - factory.setMessageConverter(new Jackson2JsonMessageConverter()); - ... - return factory; -} ----- -==== - -This configures a Jackson2 converter that expects header information to be present to guide the conversion. - -You can also use a `ContentTypeDelegatingMessageConverter`, which can handle conversion of different content types. - -Starting with version 2.3, you can override the factory converter by specifying a bean name in the `messageConverter` property. - -==== -[source, java] ----- -@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. - -NOTE: This type inference works only for `@RabbitListener` at the method level. - -See <> for more information. - -If you wish to customize the method argument converter, you can do so as follows: - -==== -[source, java] ----- -@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()); - } - - ... - -} ----- -==== - -IMPORTANT: For multi-method listeners (see <>), 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. - -[[custom-argument-resolver]] -====== Adding a Custom `HandlerMethodArgumentResolver` to @RabbitListener - -Starting with version 2.3.7 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`. - -==== -[source, java] ----- -@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) - ); - } - - } - ); - } - -} ----- -==== - -[[async-annotation-driven-registration]] -====== 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. -The following example shows how to do so: - -==== -[source,java] ----- -@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); - } -} ----- -==== - -In the preceding example, we used `SimpleRabbitListenerEndpoint`, which provides the actual `MessageListener` to invoke, but you could just as well build your own endpoint variant to describe a custom invocation mechanism. - -It should be noted that you could just as well skip the use of `@RabbitListener` altogether and register your endpoints programmatically through `RabbitListenerConfigurer`. - -[[async-annotation-driven-enable-signature]] -====== 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. -The following example rewrites it to inject the `Order` with a custom header: - -==== -[source,java] ----- -@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: - -* The raw `org.springframework.amqp.core.Message`. -* The `MessageProperties` from the raw `Message`. -* The `com.rabbitmq.client.Channel` on which the message was received. -* The `org.springframework.messaging.Message` converted from the incoming AMQP message. -* `@Header`-annotated method arguments to extract a specific header value, including standard AMQP headers. -* `@Headers`-annotated argument that must also 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. -The following example shows how to do so: - -==== -[source,java] ----- - -@RabbitListener(queues = "myQueue") -public void processOrder(Message 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, as follows: - -==== -[source,java] ----- - -@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; - } -} ----- -==== - -[[rabbit-validation]] -====== @RabbitListener @Payload Validation - -Starting with version 2.3.7, 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. - -==== -[source, java] ----- -@Configuration -@EnableRabbit -public class Config implements RabbitListenerConfigurer { - ... - @Override - public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) { - registrar.setValidator(new MyValidator()); - } -} ----- -==== - -NOTE: When using Spring Boot with the validation starter, a `LocalValidatorFactoryBean` is auto-configured: - -==== -[source, java] ----- -@Configuration -@EnableRabbit -public class Config implements RabbitListenerConfigurer { - @Autowired - private LocalValidatorFactoryBean validator; - ... - @Override - public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) { - registrar.setValidator(this.validator); - } -} ----- -==== - -To validate: - -==== -[source, java] ----- -public static class ValidatedClass { - @Max(10) - private int bar; - public int getBar() { - return this.bar; - } - public void setBar(int bar) { - this.bar = bar; - } -} ----- -==== - -and - -==== -[source, java] ----- -@RabbitListener(id="validated", queues = "queue1", errorHandler = "validationErrorHandler", - containerFactory = "jsonListenerContainerFactory") -public void validatedListener(@Payload @Valid ValidatedClass val) { - ... -} -@Bean -public RabbitListenerErrorHandler validationErrorHandler() { - return (m, e) -> { - ... - }; -} ----- -==== - -[[annotation-multiple-queues]] -====== 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. -The following example shows how to do so: - -==== -[source, java] ----- -@Component -public class MyService { - - @RabbitListener(queues = { "queue1", "queue2" } ) - public void processOrder(String data, @Header(AmqpHeaders.CONSUMER_QUEUE) String queue) { - ... - } - -} ----- -==== - -Starting with version 1.5, you can externalize the queue names by using property placeholders and SpEL. -The following example shows how to do so: - -==== -[source, java] ----- -@Component -public class MyService { - - @RabbitListener(queues = "#{'${property.with.comma.delimited.queue.names}'.split(',')}" ) - public void processOrder(String data, @Header(AmqpHeaders.CONSUMER_QUEUE) String queue) { - ... - } - -} ----- -==== - -Prior to version 1.5, only a single queue could be specified this way. -Each queue needed a separate property. - -[[async-annotation-driven-reply]] -====== 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. - -Assuming our `processOrder` method should now return an `OrderStatus`, we can write it as follows to automatically send a reply: - -==== -[source,java] ----- -@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, something like the following: - -==== -[source,java] ----- - -@RabbitListener(destination = "myQueue") -@SendTo("status") -public Message 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. -Starting with version 2.2.3, 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: - -==== -[source, java] ----- -factory.setBeforeSendReplyPostProcessors(msg -> { - msg.getMessageProperties().setHeader("calledBean", - msg.getMessageProperties().getTargetBean().getClass().getSimpleName()); - msg.getMessageProperties().setHeader("calledMethod", - msg.getMessageProperties().getTargetMethod().getName()); - return m; -}); ----- -==== - -Starting with version 2.2.5, 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. - -==== -[source, java] ----- -@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; - }; -} ----- -==== - -Starting with version 3.0, you can configure the post processor on the container factory instead of on the annotation. - -==== -[source, java] ----- -factory.setReplyPostProcessorProvider(id -> (req, resp) -> { - resp.getMessageProperties().setHeader("myHeader", req.getMessageProperties().getHeader("myHeader")); - return resp; -}); ----- -==== - -The `id` parameter is the listener id. - -A setting on the annotation will supersede the factory setting. - -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. -The valid values are as follows: - -* `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, as shown in the following example: - -==== -[source, java] ----- -@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 must evaluate to a `String`, which can be a simple queue name (sent to the default exchange) or with -the form `exchange/routingKey` as discussed prior to the preceding example. - -NOTE: 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 (described after the next example). - -Starting with version 1.6, the `@SendTo` can be a SpEL expression that is evaluated at runtime against the request -and reply, as the following example shows: - -==== -[source, java] ----- -@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. - -The context has a map property accessor, a standard type converter, and a bean resolver, which lets other beans in the -context be referenced (for example, `@someBeanName.determineReplyQ(request, 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 `@`. - -Starting with version 2.1, simple property placeholders are also supported (for example, `${some.reply.to}`). -With earlier versions, the following can be used as a work around, as the following example shows: - -==== -[source, java] ----- -@RabbitListener(queues = "foo") -@SendTo("#{environment['my.send.to']}") -public String listen(Message in) { - ... - return ... -} ----- -==== - -[[reply-content-type]] -====== 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. -This allows the converter to select the appropriate delegate converter for the reply. - -==== -[source, java] ----- -@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. - -==== -[source, java] ----- -@RabbitListener(queues = "q1", replyContentType = "application/json", - converterWinsContentType = "false") -public String listen(Thing in) { - ... - return someJsonString; -} ----- -==== - -These properties (`replyContentType` and `converterWinsContentType`) do not apply when the return type is a Spring AMQP `Message` or a Spring Messaging `Message`. -In the first case, there is no conversion involved; simply set the `contentType` message property. -In the second case, the behavior is controlled using message headers: - -==== -[source, java] ----- -@RabbitListener(queues = "q1", messageConverter = "delegating") -@SendTo("q2") -public Message listen(String in) { - ... - return MessageBuilder.withPayload(in.toUpperCase()) - .setHeader(MessageHeaders.CONTENT_TYPE, "application/xml") - .build(); -} ----- -==== - -This content type will be passed in the `MessageProperties` to the converter. -By default, for backwards compatibility, any content type property set by the converter will be overwritten by this value after conversion. -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. - -[[annotation-method-selection]] -====== Multi-method Listeners - -Starting with version 1.5.0, 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. -This is best described using an example: - -==== -[source, java] ----- -@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` (<>). - -Starting with version 2.0.3, 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. - -IMPORTANT: `@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. - -[[repeatable-rabbit-listener]] -====== `@Repeatable` `@RabbitListener` - -Starting with version 1.6, 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, each of which invokes the same listener -`@Bean`. -Repeatable annotations can be used with Java 8 or above. - -====== 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. -Consider the following example: - -==== -[source, java] ----- -interface TxService

{ - - String handle(P payload, String header); - -} - -static class TxServiceImpl implements TxService { - - @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, as the following example shows: - -==== -[source, java] ----- -static class TxServiceImpl implements TxService { - - @Override - @Transactional - @RabbitListener(...) - public String handle(@Payload Foo foo, @Header("amqp_receivedRoutingKey") String rk) { - ... - } - -} ----- -==== - -[[annotation-error-handling]] -====== 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. - -Starting with version 2.0, the `@RabbitListener` annotation has two new attributes: `errorHandler` and `returnExceptions`. - -These are not configured by default. - -You can use the `errorHandler` to provide the bean name of a `RabbitListenerErrorHandler` implementation. -This functional interface has one method, as follows: - -[source, java] ----- -@FunctionalInterface -public interface RabbitListenerErrorHandler { - - Object handleError(Message amqpMessage, org.springframework.messaging.Message message, - ListenerExecutionFailedException exception) throws Exception; - -} ----- - -As you can see, you have access to the raw message received from the container, the spring-messaging `Message` object produced by the message converter, and 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. - -IMPORTANT: 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. - -IMPORTANT: In version 2.1, this interface moved from package `o.s.amqp.rabbit.listener` to `o.s.amqp.rabbit.listener.api`. - -Starting with version 2.1.7, the `Channel` is available in a messaging message header; this allows you to ack or nack the failed messasge when using `AcknowledgeMode.MANUAL`: - -==== -[source, java] ----- -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); - } ----- -==== - -Starting with version 2.2.18, 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. -Previously, such errors were thrown and handled by the container. - -====== 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. -You can then iterate over this collection, for example, to stop or start all containers or invoke the `Lifecycle` methods -on the registry itself, which will invoke the operations on each container. - -You can also get a reference to an individual container by using its `id`, using `getListenerContainer(String id)` -- for -example, `registry.getListenerContainer("multi")` for the container created by the snippet above. - -Starting with version 1.5.2, you can obtain the `id` values of the registered containers with `getListenerContainerIds()`. - -Starting with version 1.5, you can now assign a `group` to the container on the `RabbitListener` endpoint. -This provides a mechanism to get a reference to a subset of containers. -Adding a `group` attribute causes a bean of type `Collection` to be registered with the context with the group name. - -By default, stopping a container will cancel the consumer and process all prefetched messages before stopping. -Starting with versions 2.4.14, 3.0.6, you can set the <> 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. - -[[receiving-batch]] -===== @RabbitListener with Batching - -When receiving a <> 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` or `Collection`: - -==== -[source, java] ----- -@Bean -public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() { - SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); - factory.setConnectionFactory(connectionFactory()); - factory.setBatchListener(true); - return factory; -} - -@RabbitListener(queues = "batch.1") -public void listen1(List in) { - ... -} - -// or - -@RabbitListener(queues = "batch.2") -public void listen2(List> 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 <>. - -Also starting with version 2.2. 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. - -==== -[source, java] ----- -@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`: - -==== -[source, java] ----- -@RabbitListener(queues = "batch.1", containerFactory = "consumerBatchContainerFactory") -public void consumerBatch1(List amqpMessages) { - ... -} - -@RabbitListener(queues = "batch.2", containerFactory = "consumerBatchContainerFactory") -public void consumerBatch2(List> messages) { - ... -} - -@RabbitListener(queues = "batch.3", containerFactory = "consumerBatchContainerFactory") -public void consumerBatch3(List strings) { - ... -} ----- -==== - -* the first is called with the raw, unconverted `org.springframework.amqp.core.Message` s received. -* the second is called with the `org.springframework.messaging.Message` s with converted payloads and mapped headers/properties. -* the third is called with the converted payloads, with no access to headers/properties. - -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 - -Listener container factories were introduced to support the `@RabbitListener` and registering containers with the `RabbitListenerEndpointRegistry`, as discussed in <>. - -Starting with version 2.1, 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 -* Add the listener after creation - -The following example shows how to use a `SimpleRabbitListenerEndpoint` to create a listener container: - -==== -[source, java] ----- -@Bean -public SimpleMessageListenerContainer factoryCreatedContainerSimpleListener( - SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory) { - SimpleRabbitListenerEndpoint endpoint = new SimpleRabbitListenerEndpoint(); - endpoint.setQueueNames("queue.1"); - endpoint.setMessageListener(message -> { - ... - }); - return rabbitListenerContainerFactory.createListenerContainer(endpoint); -} ----- -==== - -The following example shows how to add the listener after creation: - -==== -[source, java] ----- -@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`. - -These techniques are useful if you wish to create several containers with similar properties or use a pre-configured container factory such as the one provided by Spring Boot auto configuration or both. - -IMPORTANT: Containers created this way are normal `@Bean` instances and are not registered in the `RabbitListenerEndpointRegistry`. - -[[async-returns]] -===== Asynchronous `@RabbitListener` Return Types - -`@RabbitListener` (and `@RabbitHandler`) methods can be specified with asynchronous return types `CompletableFuture` and `Mono`, letting the reply be sent asynchronously. -`ListenableFuture` is no longer supported; it has been deprecated by Spring Framework. - -IMPORTANT: 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. - -Starting with version 3.0.5, the `@RabbitListener` (and `@RabbitHandler`) methods can be marked with Kotlin `suspend` and the whole handling process and reply producing (optional) happens on respective Kotlin coroutine. -All the mentioned rules about `AcknowledgeMode.MANUAL` are still apply. -The `org.jetbrains.kotlinx:kotlinx-coroutines-reactor` dependency must be present in classpath to allow `suspend` function invocations. - -Also starting with version 3.0.5, if a `RabbitListenerErrorHandler` is configured on a listener with an async return type (including Kotlin suspend functions), the error handler is invoked after a failure. -See <> for more information about this error handler and its purpose. - -[[threading]] -===== Threading and Asynchronous Consumers - -A number of different threads are involved with 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. -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. - -NOTE: When using the default `SimpleAsyncTaskExecutor`, for the threads the listener is invoked on, the listener container `beanName` is used in the `threadNamePrefix`. -This is useful for log analysis. -We generally recommend always including the thread name in the logging appender configuration. -When a `TaskExecutor` is specifically provided through the `taskExecutor` property on the container, it is used as is, without modification. -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. - -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 this is not configured, the client uses an internal thread pool executor with (at the time of writing) a pool size of `Runtime.getRuntime().availableProcessors() * 2` for each connection. - -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. - -IMPORTANT: 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 default pool size (at the time of writing) is `Runtime.getRuntime().availableProcessors() * 2`. - -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`, as discussed in <>. - -[[choose-container]] -===== Choosing a Container - -Version 2.0 introduced the `DirectMessageListenerContainer` (DMLC). -Previously, only the `SimpleMessageListenerContainer` (SMLC) was available. -The SMLC 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 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 <> 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. -* Threads are shared across consumers rather than having a dedicated thread for each consumer in the SMLC. -However, see the IMPORTANT note about the connection factory configuration in <>. - -See <> for information about which configuration properties apply to each container. - -[[idle-containers]] -===== 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. - -Starting with version 1.6, 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. - -To configure this feature, set `idleEventInterval` on the container. -The following example shows how to do so in XML and in Java (for both a `SimpleMessageListenerContainer` and a `SimpleRabbitListenerContainerFactory`): - -==== -[source, xml] ----- - - - ----- - -[source, java] ----- -@Bean -public SimpleMessageListenerContainer(ConnectionFactory connectionFactory) { - SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); - ... - container.setIdleEventInterval(60000L); - ... - return container; -} ----- - -[source, java] ----- -@Bean -public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() { - SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); - factory.setConnectionFactory(rabbitConnectionFactory()); - factory.setIdleEventInterval(60000L); - ... - return factory; -} ----- -==== - -In each of these cases, an event is published once per minute while the container is idle. - -====== Event Consumption - -You can capture idle events by implementing `ApplicationListener` -- either a general listener, or one narrowed to only -receive this specific event. -You can also use `@EventListener`, introduced in Spring Framework 4.2. - -The following example combines the `@RabbitListener` and `@EventListener` into a single class. -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 - -The following example shows how to create listeners by using both the `@RabbitListener` and the `@EventListener` annotations: - -==== -[source, Java] ----- -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) { - ... - } - -} ----- -==== - -IMPORTANT: Event listeners see events for all containers. -Consequently, in the preceding example, we narrow the events received based on the listener ID. - -CAUTION: 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. - -[[micrometer]] -===== Monitoring Listener Performance - -Starting with version 2.2, the listener containers will automatically create and update Micrometer `Timer` s for the listener, if `Micrometer` is detected on the class path, and a single `MeterRegistry` is present in the application context (or exactly one is annotated `@Primary`, such as when using Spring Boot). -The timers can be disabled by setting the container property `micrometerEnabled` to `false`. - -Two timers are maintained - one for successful calls to the listener and one for failures. -With a simple `MessageListener`, there is a pair of timers for each configured queue. - -The timers are named `spring.rabbitmq.listener` and have the following tags: - -* `listenerId` : (listener id or container bean name) -* `queue` : (the queue name for a simple listener or list of configured queue names when `consumerBatchEnabled` is `true` - because a batch may contain messages from multiple queues) -* `result` : `success` or `failure` -* `exception` : `none` or `ListenerExecutionFailedException` - -You can add additional tags using the `micrometerTags` container property. - -Also see <>. - -[[micrometer-observation]] -===== Micrometer Observation - -Using Micrometer for observation is now supported, since version 3.0, for the `RabbitTemplate` and listener containers. - -Set `observationEnabled` on each component to enable observation; this will disable <> because the timers will now be managed with each observation. -When using annotated listeners, set `observationEnabled` on the container factory. - -Refer to https://micrometer.io/docs/tracing[Micrometer Tracing] for more information. - -To add tags to timers/traces, configure a custom `RabbitTemplateObservationConvention` or `RabbitListenerObservationConvention` to the template or listener container, respectively. - -The default implementations add the `name` tag for template observations and `listener.id` tag for containers. - -You can either subclass `DefaultRabbitTemplateObservationConvention` or `DefaultRabbitListenerObservationConvention` or provide completely new implementations. - -See <> for more details. - -[[containers-and-broker-named-queues]] -==== Containers and Broker-Named queues - -While it is preferable to use `AnonymousQueue` instances as auto-delete queues, starting with version 2.1, you can use broker named queues with listener containers. -The following example shows how to do so: - -==== -[source, java] ----- -@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. - -NOTE: You cannot add broker-named queues to the containers while they are running. - -IMPORTANT: 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. - -[[message-converters]] -==== Message Converters - -The `AmqpTemplate` also defines several methods for sending and receiving messages that delegate to a `MessageConverter`. -The `MessageConverter` provides a single method for each direction: one for converting *to* a `Message` and another for converting *from* a `Message`. -Notice that, when converting to a `Message`, you can also provide properties in addition to the object. -The `object` parameter typically corresponds to the Message body. -The following listing shows the `MessageConverter` interface definition: - -==== -[source,java] ----- -public interface MessageConverter { - - Message toMessage(Object object, MessageProperties messageProperties) - throws MessageConversionException; - - Object fromMessage(Message message) throws MessageConversionException; - -} ----- -==== - -The relevant `Message`-sending methods on the `AmqpTemplate` are simpler than the methods we discussed previously, because they do not require the `Message` instance. -Instead, the `MessageConverter` is responsible for "`creating`" each `Message` by converting the provided object to the byte array for the `Message` body and then adding any provided `MessageProperties`. -The following listing shows the definitions of the various methods: - -==== -[source,java] ----- -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; ----- -==== - -On the receiving side, there are only two methods: one that accepts the queue name and one that relies on the template's "`queue`" property having been set. -The following listing shows the definitions of the two methods: - -==== -[source,java] ----- -Object receiveAndConvert() throws AmqpException; - -Object receiveAndConvert(String queueName) throws AmqpException; ----- -==== - -NOTE: The `MessageListenerAdapter` mentioned in <> also uses a `MessageConverter`. - -[[simple-message-converter]] -===== `SimpleMessageConverter` - -The default implementation of the `MessageConverter` strategy is called `SimpleMessageConverter`. -This is the converter that is used by an instance of `RabbitTemplate` if you do not explicitly configure an alternative. -It handles text-based content, serialized Java objects, and byte arrays. - -====== Converting From a `Message` - -If the content type of the input `Message` begins with "text" (for example, -"text/plain"), it also checks for the content-encoding property to determine the charset to be used when converting the `Message` body byte array to a Java `String`. -If no content-encoding property had been set on the input `Message`, it uses the UTF-8 charset by default. -If you need to override that default setting, you can configure an instance of `SimpleMessageConverter`, set its `defaultCharset` property, and inject that into a `RabbitTemplate` instance. - -If the content-type property value of the input `Message` is set to "application/x-java-serialized-object", the `SimpleMessageConverter` tries to deserialize (rehydrate) the byte array into a Java object. -While that might be useful for simple prototyping, we do not recommend relying on Java serialization, since it leads to tight coupling between the producer and the consumer. -Of course, it also rules out usage of non-Java systems on either side. -With AMQP being a wire-level protocol, it would be unfortunate to lose much of that advantage with such restrictions. -In the next two sections, we explore some alternatives for passing rich domain object content without relying on Java serialization. - -For all other content-types, the `SimpleMessageConverter` returns the `Message` body content directly as a byte array. - -See <> for important information. - -====== Converting To a `Message` - -When converting to a `Message` from an arbitrary Java Object, the `SimpleMessageConverter` likewise deals with byte arrays, strings, and serializable instances. -It converts each of these to bytes (in the case of byte arrays, there is nothing to convert), and it sets the content-type property accordingly. -If the `Object` to be converted does not match one of those types, the `Message` body is null. - -[[serializer-message-converter]] -===== `SerializerMessageConverter` - -This converter is similar to the `SimpleMessageConverter` except that it can be configured with other Spring Framework -`Serializer` and `Deserializer` implementations for `application/x-java-serialized-object` conversions. - -See <> for important information. - -[[json-message-converter]] -===== Jackson2JsonMessageConverter - -This section covers using the `Jackson2JsonMessageConverter` to convert to and from a `Message`. -It has the following sections: - -* <> -* <> - -[[Jackson2JsonMessageConverter-to-message]] -====== Converting to a `Message` - -As mentioned in the previous section, relying on Java serialization is generally not recommended. -One rather common alternative that is more flexible and portable across different languages and platforms is JSON -(JavaScript Object Notation). -The converter can be configured on any `RabbitTemplate` instance to override its usage of the `SimpleMessageConverter` -default. -The `Jackson2JsonMessageConverter` uses the `com.fasterxml.jackson` 2.x library. -The following example configures a `Jackson2JsonMessageConverter`: - -==== -[source,xml] ----- - - - - - - - - - ----- -==== - -As shown above, `Jackson2JsonMessageConverter` uses a `DefaultClassMapper` by default. -Type information is added to (and retrieved from) `MessageProperties`. -If an inbound message does not contain type information in `MessageProperties`, but you know the expected type, you -can configure a static type by using the `defaultType` property, as the following example shows: - -==== -[source,xml] ----- - - - - - - - ----- -==== - -In addition, you can provide custom mappings from the value in the `__TypeId__` header. -The following example shows how to do so: - -==== -[source, java] ----- -@Bean -public Jackson2JsonMessageConverter jsonMessageConverter() { - Jackson2JsonMessageConverter jsonConverter = new Jackson2JsonMessageConverter(); - jsonConverter.setClassMapper(classMapper()); - return jsonConverter; -} - -@Bean -public DefaultClassMapper classMapper() { - DefaultClassMapper classMapper = new DefaultClassMapper(); - Map> idClassMapping = new HashMap<>(); - idClassMapping.put("thing1", Thing1.class); - idClassMapping.put("thing2", Thing2.class); - classMapper.setIdClassMapping(idClassMapping); - return classMapper; -} ----- -==== - -Now, if the sending system sets the header to `thing1`, the converter creates a `Thing1` object, and so on. -See the <> sample application for a complete discussion about converting messages from non-Spring applications. - -Starting with version 2.4.3, the converter will not add a `contentEncoding` message property if the `supportedMediaType` has a `charset` parameter; this is also used for the encoding. -A new method `setSupportedMediaType` has been added: - -==== -[source, java] ----- -String utf16 = "application/json; charset=utf-16"; -converter.setSupportedContentType(MimeTypeUtils.parseMimeType(utf16)); ----- -==== - -[[Jackson2JsonMessageConverter-from-message]] -====== Converting from a `Message` - -Inbound messages are converted to objects according to the type information added to headers by the sending system. - -Starting with version 2.4.3, if there is no `contentEncoding` message property, the converter will attempt to detect a `charset` parameter in the `contentType` message property and use that. -If neither exist, if the `supportedMediaType` has a `charset` parameter, it will be used for decoding, with a final fallback to the `defaultCharset` property. -A new method `setSupportedMediaType` has been added: - -==== -[source, java] ----- -String utf16 = "application/json; charset=utf-16"; -converter.setSupportedContentType(MimeTypeUtils.parseMimeType(utf16)); ----- -==== - -In versions prior to 1.6, if type information is not present, conversion would fail. -Starting with version 1.6, if type information is missing, the converter converts the JSON by using Jackson defaults (usually a map). - -Also, starting with version 1.6, when you use `@RabbitListener` annotations (on methods), the inferred type information is added to the `MessageProperties`. -This lets the converter convert to the argument type of the target method. -This only applies if there is one parameter with no annotations or a single parameter with the `@Payload` annotation. -Parameters of type `Message` are ignored during the analysis. - -IMPORTANT: By default, the inferred type information will override the inbound `__TypeId__` and related headers created -by the sending system. -This lets the receiving system automatically convert to a different domain object. -This applies only if the parameter type is concrete (not abstract or an interface) or it is from the `java.util` -package. -In all other cases, the `__TypeId__` and related headers is used. -There are cases where you might wish to override the default behavior and always use the `__TypeId__` information. -For example, suppose you have a `@RabbitListener` that takes a `Thing1` argument but the message contains a `Thing2` that -is a subclass of `Thing1` (which is concrete). -The inferred type would be incorrect. -To handle this situation, set the `TypePrecedence` property on the `Jackson2JsonMessageConverter` to `TYPE_ID` instead -of the default `INFERRED`. -(The property is actually on the converter's `DefaultJackson2JavaTypeMapper`, but a setter is provided on the converter -for convenience.) -If you inject a custom type mapper, you should set the property on the mapper instead. - -NOTE: When converting from the `Message`, an incoming `MessageProperties.getContentType()` must be JSON-compliant (`contentType.contains("json")` is used to check). -Starting with version 2.2, `application/json` is assumed if there is no `contentType` property, or it has the default value `application/octet-stream`. -To revert to the previous behavior (return an unconverted `byte[]`), set the converter's `assumeSupportedContentType` property to `false`. -If the content type is not supported, a `WARN` log message `Could not convert incoming message with content-type [...]`, is emitted and `message.getBody()` is returned as is -- as a `byte[]`. -So, to meet the `Jackson2JsonMessageConverter` requirements on the consumer side, the producer must add the `contentType` message property -- for example, as `application/json` or `text/x-json` or by using the `Jackson2JsonMessageConverter`, which sets the header automatically. -The following listing shows a number of converter calls: - -==== -[source, java] ----- -@RabbitListener -public void thing1(Thing1 thing1) {...} - -@RabbitListener -public void thing1(@Payload Thing1 thing1, @Header("amqp_consumerQueue") String queue) {...} - -@RabbitListener -public void thing1(Thing1 thing1, o.s.amqp.core.Message message) {...} - -@RabbitListener -public void thing1(Thing1 thing1, o.s.messaging.Message message) {...} - -@RabbitListener -public void thing1(Thing1 thing1, String bar) {...} - -@RabbitListener -public void thing1(Thing1 thing1, o.s.messaging.Message message) {...} ----- -==== - -In the first four cases in the preceding listing, the converter tries to convert to the `Thing1` type. -The fifth example is invalid because we cannot determine which argument should receive the message payload. -With the sixth example, the Jackson defaults apply due to the generic type being a `WildcardType`. - -You can, however, create a custom converter and use the `targetMethod` message property to decide which type to convert -the JSON to. - -NOTE: This type inference can only be achieved when the `@RabbitListener` annotation is declared at the method level. -With class-level `@RabbitListener`, the converted type is used to select which `@RabbitHandler` method to invoke. -For this reason, the infrastructure provides the `targetObject` message property, which you can use in a custom -converter to determine the type. - -IMPORTANT: Starting with version 1.6.11, `Jackson2JsonMessageConverter` and, therefore, `DefaultJackson2JavaTypeMapper` (`DefaultClassMapper`) provide the `trustedPackages` option to overcome https://pivotal.io/security/cve-2017-4995[Serialization Gadgets] vulnerability. -By default and for backward compatibility, the `Jackson2JsonMessageConverter` trusts all packages -- that is, it uses `*` for the option. - -Starting with version 2.4.7, the converter can be configured to return `Optional.empty()` if Jackson returns `null` after deserializing the message body. -This facilitates `@RabbitListener` s to receive null payloads, in two ways: - -==== -[source, java] ----- -@RabbitListener(queues = "op.1") -void listen(@Payload(required = false) Thing payload) { - handleOptional(payload); // payload might be null -} - -@RabbitListener(queues = "op.2") -void listen(Optional optional) { - handleOptional(optional.orElse(this.emptyThing)); -} ----- -==== - -To enable this feature, set `setNullAsOptionalEmpty` to `true`; when `false` (default), the converter falls back to the raw message body (`byte[]`). - -==== -[source, java] ----- -@Bean -Jackson2JsonMessageConverter converter() { - Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter(); - converter.setNullAsOptionalEmpty(true); - return converter; -} ----- -==== - -[[jackson-abstract]] -====== Deserializing Abstract Classes - -Prior to version 2.2.8, if the inferred type of a `@RabbitListener` was an abstract class (including interfaces), the converter would fall back to looking for type information in the headers and, if present, used that information; if that was not present, it would try to create the abstract class. -This caused a problem when a custom `ObjectMapper` that is configured with a custom deserializer to handle the abstract class is used, but the incoming message has invalid type headers. - -Starting with version 2.2.8, the previous behavior is retained by default. If you have such a custom `ObjectMapper` and you want to ignore type headers, and always use the inferred type for conversion, set the `alwaysConvertToInferredType` to `true`. -This is needed for backwards compatibility and to avoid the overhead of an attempted conversion when it would fail (with a standard `ObjectMapper`). - -[[data-projection]] -====== Using Spring Data Projection Interfaces - -Starting with version 2.2, you can convert JSON to a Spring Data Projection interface instead of a concrete type. -This allows very selective, and low-coupled bindings to data, including the lookup of values from multiple places inside the JSON document. -For example the following interface can be defined as message payload type: - -==== -[source, java] ----- -interface SomeSample { - - @JsonPath({ "$.username", "$.user.name" }) - String getUsername(); - -} ----- -==== - -==== -[source, java] ----- -@RabbitListener(queues = "projection") -public void projection(SomeSample in) { - String username = in.getUsername(); - ... -} ----- -==== - -Accessor methods will be used to lookup the property name as field in the received JSON document by default. -The `@JsonPath` expression allows customization of the value lookup, and even to define multiple JSON path expressions, to lookup values from multiple places until an expression returns an actual value. - -To enable this feature, set the `useProjectionForInterfaces` to `true` on the message converter. -You must also add `spring-data:spring-data-commons` and `com.jayway.jsonpath:json-path` to the class path. - -When used as the parameter to a `@RabbitListener` method, the interface type is automatically passed to the converter as normal. - -[[json-complex]] -====== Converting From a `Message` With `RabbitTemplate` - -As mentioned earlier, type information is conveyed in message headers to assist the converter when converting from a message. -This works fine in most cases. -However, when using generic types, it can only convert simple objects and known "`container`" objects (lists, arrays, and maps). -Starting with version 2.0, the `Jackson2JsonMessageConverter` implements `SmartMessageConverter`, which lets it be used with the new `RabbitTemplate` methods that take a `ParameterizedTypeReference` argument. -This allows conversion of complex generic types, as shown in the following example: - -==== -[source, java] ----- -Thing1> thing1 = - rabbitTemplate.receiveAndConvert(new ParameterizedTypeReference>>() { }); ----- -==== - -NOTE: Starting with version 2.1, the `AbstractJsonMessageConverter` class has been removed. -It is no longer the base class for `Jackson2JsonMessageConverter`. -It has been replaced by `AbstractJackson2MessageConverter`. - -===== `MarshallingMessageConverter` - -Yet another option is the `MarshallingMessageConverter`. -It delegates to the Spring OXM library's implementations of the `Marshaller` and `Unmarshaller` strategy interfaces. -You can read more about that library https://docs.spring.io/spring/docs/current/spring-framework-reference/html/oxm.html[here]. -In terms of configuration, it is most common to provide only the constructor argument, since most implementations of `Marshaller` also implement `Unmarshaller`. -The following example shows how to configure a `MarshallingMessageConverter`: - -==== -[source,xml] ----- - - - - - - - - ----- -==== - -[[jackson2xml]] -===== `Jackson2XmlMessageConverter` - -This class was introduced in version 2.1 and can be used to convert messages from and to XML. - -Both `Jackson2XmlMessageConverter` and `Jackson2JsonMessageConverter` have the same base class: `AbstractJackson2MessageConverter`. - -NOTE: The `AbstractJackson2MessageConverter` class is introduced to replace a removed class: `AbstractJsonMessageConverter`. - -The `Jackson2XmlMessageConverter` uses the `com.fasterxml.jackson` 2.x library. - -You can use it the same way as `Jackson2JsonMessageConverter`, except it supports XML instead of JSON. -The following example configures a `Jackson2JsonMessageConverter`: - -[source,xml] ----- - - - - - - - ----- -See <> for more information. - -NOTE: Starting with version 2.2, `application/xml` is assumed if there is no `contentType` property, or it has the default value `application/octet-stream`. -To revert to the previous behavior (return an unconverted `byte[]`), set the converter's `assumeSupportedContentType` property to `false`. - -===== `ContentTypeDelegatingMessageConverter` - -This class was introduced in version 1.4.2 and allows delegation to a specific `MessageConverter` based on the content type property in the `MessageProperties`. -By default, it delegates to a `SimpleMessageConverter` if there is no `contentType` property or there is a value that matches none of the configured converters. -The following example configures a `ContentTypeDelegatingMessageConverter`: - -==== -[source,xml] ----- - - - - - - - - ----- -==== - -[[java-deserialization]] -===== Java Deserialization - -This section covers how to deserialize Java objects. - -[IMPORTANT] -==== -There is a possible vulnerability when deserializing java objects from untrusted sources. - -If you accept messages from untrusted sources with a `content-type` of `application/x-java-serialized-object`, you should -consider configuring which packages and classes are allowed to be deserialized. -This applies to both the `SimpleMessageConverter` and `SerializerMessageConverter` when it is configured to use a -`DefaultDeserializer` either implicitly or via configuration. - -By default, the allowed list is empty, meaning no classes will be deserialized. - -You can set a list of patterns, such as `thing1.*`, `thing1.thing2.Cat` or `*.MySafeClass`. - -The patterns are checked in order until a match is found. -If there is no match, a `SecurityException` is thrown. - -You can set the patterns using the `allowedListPatterns` property on these converters. -Alternatively, if you trust all message originators, you can set the environment variable `SPRING_AMQP_DESERIALIZATION_TRUST_ALL` or system property `spring.amqp.deserialization.trust.all` to `true`. -==== - -[[message-properties-converters]] -===== Message Properties Converters - -The `MessagePropertiesConverter` strategy interface is used to convert between the Rabbit Client `BasicProperties` and Spring AMQP `MessageProperties`. -The default implementation (`DefaultMessagePropertiesConverter`) is usually sufficient for most purposes, but you can implement your own if needed. -The default properties converter converts `BasicProperties` elements of type `LongString` to `String` instances when the size is not greater than `1024` bytes. -Larger `LongString` instances are not converted (see the next paragraph). -This limit can be overridden with a constructor argument. - -Starting with version 1.6, headers longer than the long string limit (default: 1024) are now left as -`LongString` instances by default by the `DefaultMessagePropertiesConverter`. -You can access the contents through the `getBytes[]`, `toString()`, or `getStream()` methods. - -Previously, the `DefaultMessagePropertiesConverter` "`converted`" such headers to a `DataInputStream` (actually it just referenced the `LongString` instance's `DataInputStream`). -On output, this header was not converted (except to a String -- for example, `java.io.DataInputStream@1d057a39` by calling `toString()` on the stream). - -Large incoming `LongString` headers are now correctly "`converted`" on output, too (by default). - -A new constructor is provided to let you configure the converter to work as before. -The following listing shows the Javadoc comment and declaration of the method: - -==== -[source, java] ----- -/** - * 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) { ... } ----- -==== - -Also starting with version 1.6, a new property called `correlationIdString` has been added to `MessageProperties`. -Previously, when converting to and from `BasicProperties` used by the RabbitMQ client, an unnecessary `byte[] <-> String` conversion was performed because `MessageProperties.correlationId` is a `byte[]`, but `BasicProperties` uses a `String`. -(Ultimately, the RabbitMQ client uses UTF-8 to convert the `String` to bytes to put in the protocol message). - -To provide maximum backwards compatibility, a new property called `correlationIdPolicy` has been added to the -`DefaultMessagePropertiesConverter`. -This takes a `DefaultMessagePropertiesConverter.CorrelationIdPolicy` enum argument. -By default it is set to `BYTES`, which replicates the previous behavior. - -For inbound messages: - -* `STRING`: Only the `correlationIdString` property is mapped -* `BYTES`: Only the `correlationId` property is mapped -* `BOTH`: Both properties are mapped - -For outbound messages: - -* `STRING`: Only the `correlationIdString` property is mapped -* `BYTES`: Only the `correlationId` property is mapped -* `BOTH`: Both properties are considered, with the `String` property taking precedence - -Also starting with version 1.6, the inbound `deliveryMode` property is no longer mapped to `MessageProperties.deliveryMode`. -It is mapped to `MessageProperties.receivedDeliveryMode` instead. -Also, the inbound `userId` property is no longer mapped to `MessageProperties.userId`. -It is mapped to `MessageProperties.receivedUserId` instead. -These changes are to avoid unexpected propagation of these properties if the same `MessageProperties` object is used for an outbound message. - -Starting with version 2.2, the `DefaultMessagePropertiesConverter` converts any custom headers with values of type `Class` using `getName()` instead of `toString()`; this avoids consuming application having to parse the class name out of the `toString()` representation. -For rolling upgrades, you may need to change your consumers to understand both formats until all producers are upgraded. - -[[post-processing]] -==== Modifying Messages - Compression and More - -A number of extension points exist. -They let you perform some processing on a message, either before it is sent to RabbitMQ or immediately after it is received. - -As can be seen in <>, one such extension point is in the `AmqpTemplate` `convertAndReceive` operations, where you can provide a `MessagePostProcessor`. -For example, after your POJO has been converted, the `MessagePostProcessor` lets you set custom headers or properties on the `Message`. - -Starting with version 1.4.2, additional extension points have been added to the `RabbitTemplate` - `setBeforePublishPostProcessors()` and `setAfterReceivePostProcessors()`. -The first enables a post processor to run immediately before sending to RabbitMQ. -When using batching (see <>), this is invoked after the batch is assembled and before the batch is sent. -The second is invoked immediately after a message is received. - -These extension points are used for such features as compression and, for this purpose, several `MessagePostProcessor` implementations are provided. -`GZipPostProcessor`, `ZipPostProcessor` and `DeflaterPostProcessor` compress messages before sending, and `GUnzipPostProcessor`, `UnzipPostProcessor` and `InflaterPostProcessor` decompress received messages. - -NOTE: Starting with version 2.1.5, the `GZipPostProcessor` can be configured with the `copyProperties = true` option to make a copy of the original message properties. -By default, these properties are reused for performance reasons, and modified with compression content encoding and the optional `MessageProperties.SPRING_AUTO_DECOMPRESS` header. -If you retain a reference to the original outbound message, its properties will change as well. -So, if your application retains a copy of an outbound message with these message post processors, consider turning the `copyProperties` option on. - -IMPORTANT: Starting with version 2.2.12, you can configure the delimiter that the compressing post processors use between content encoding elements. -With versions 2.2.11 and before, this was hard-coded as `:`, it is now set to `, ` by default. -The decompressors will work with both delimiters. -However, if you publish messages with 2.3 or later and consume with 2.2.11 or earlier, you MUST set the `encodingDelimiter` property on the compressor(s) to `:`. -When your consumers are upgraded to 2.2.11 or later, you can revert to the default of `, `. - -Similarly, the `SimpleMessageListenerContainer` also has a `setAfterReceivePostProcessors()` method, letting the decompression be performed after messages are received by the container. - -Starting with version 2.1.4, `addBeforePublishPostProcessors()` and `addAfterReceivePostProcessors()` have been added to the `RabbitTemplate` to allow appending new post processors to the list of before publish and after receive post processors respectively. -Also there are methods provided to remove the post processors. -Similarly, `AbstractMessageListenerContainer` also has `addAfterReceivePostProcessors()` and `removeAfterReceivePostProcessor()` methods added. -See the Javadoc of `RabbitTemplate` and `AbstractMessageListenerContainer` for more detail. - -[[request-reply]] -==== Request/Reply Messaging - -The `AmqpTemplate` also provides a variety of `sendAndReceive` methods that accept the same argument options that were described earlier for the one-way send operations (`exchange`, `routingKey`, and `Message`). -Those methods are quite useful for request-reply scenarios, since they handle the configuration of the necessary `reply-to` property before sending and can listen for the reply message on an exclusive queue that is created internally for that purpose. - -Similar request-reply methods are also available where the `MessageConverter` is applied to both the request and reply. -Those methods are named `convertSendAndReceive`. -See the https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/core/AmqpTemplate.html[Javadoc of `AmqpTemplate`] for more detail. - -Starting with version 1.5.0, each of the `sendAndReceive` method variants has an overloaded version that takes `CorrelationData`. -Together with a properly configured connection factory, this enables the receipt of publisher confirms for the send side of the operation. -See <> and the https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/rabbit/core/RabbitOperations.html[Javadoc for `RabbitOperations`] for more information. - -Starting with version 2.0, there are variants of these methods (`convertSendAndReceiveAsType`) that take an additional `ParameterizedTypeReference` argument to convert complex returned types. -The template must be configured with a `SmartMessageConverter`. -See <> for more information. - -Starting with version 2.1, you can configure the `RabbitTemplate` with the `noLocalReplyConsumer` option to control a `noLocal` flag for reply consumers. -This is `false` by default. - -[[reply-timeout]] -===== Reply Timeout - -By default, the send and receive methods timeout after five seconds and return null. -You can modify this behavior by setting the `replyTimeout` property. -Starting with version 1.5, if you set the `mandatory` property to `true` (or the `mandatory-expression` evaluates to `true` for a particular message), if the message cannot be delivered to a queue, an `AmqpMessageReturnedException` is thrown. -This exception has `returnedMessage`, `replyCode`, and `replyText` properties, as well as the `exchange` and `routingKey` used for the send. - -NOTE: This feature uses publisher returns. -You can enable it by setting `publisherReturns` to `true` on the `CachingConnectionFactory` (see <>). -Also, you must not have registered your own `ReturnCallback` with the `RabbitTemplate`. - -Starting with version 2.1.2, a `replyTimedOut` method has been added, letting subclasses be informed of the timeout so that they can clean up any retained state. - -Starting with versions 2.0.11 and 2.1.3, when you use the default `DirectReplyToMessageListenerContainer`, you can add an error handler by setting the template's `replyErrorHandler` property. -This error handler is invoked for any failed deliveries, such as late replies and messages received without a correlation header. -The exception passed in is a `ListenerExecutionFailedException`, which has a `failedMessage` property. - -[[direct-reply-to]] -===== RabbitMQ Direct reply-to - -IMPORTANT: Starting with version 3.4.0, the RabbitMQ server supports https://www.rabbitmq.com/direct-reply-to.html[direct reply-to]. -This eliminates the main reason for a fixed reply queue (to avoid the need to create a temporary queue for each request). -Starting with Spring AMQP version 1.4.1 direct reply-to is used by default (if supported by the server) instead of creating temporary reply queues. -When no `replyQueue` is provided (or it is set with a name of `amq.rabbitmq.reply-to`), the `RabbitTemplate` automatically detects whether direct reply-to is supported and either uses it or falls back to using a temporary reply queue. -When using direct reply-to, a `reply-listener` is not required and should not be configured. - -Reply listeners are still supported with named queues (other than `amq.rabbitmq.reply-to`), allowing control of reply concurrency and so on. - -Starting with version 1.6, if you wish to use a temporary, exclusive, auto-delete queue for each -reply, set the `useTemporaryReplyQueues` property to `true`. -This property is ignored if you set a `replyAddress`. - -You can change the criteria that dictate whether to use direct reply-to by subclassing `RabbitTemplate` and overriding `useDirectReplyTo()` to check different criteria. -The method is called once only, when the first request is sent. - -Prior to version 2.0, the `RabbitTemplate` created a new consumer for each request and canceled the consumer when the reply was received (or timed out). -Now the template uses a `DirectReplyToMessageListenerContainer` instead, letting the consumers be reused. -The template still takes care of correlating the replies, so there is no danger of a late reply going to a different sender. -If you want to revert to the previous behavior, set the `useDirectReplyToContainer` (`direct-reply-to-container` when using XML configuration) property to false. - -The `AsyncRabbitTemplate` has no such option. -It always used a `DirectReplyToContainer` for replies when direct reply-to is used. - -Starting with version 2.3.7, the template has a new property `useChannelForCorrelation`. -When this is `true`, the server does not have to copy the correlation id from the request message headers to the reply message. -Instead, the channel used to send the request is used to correlate the reply to the request. - -===== Message Correlation With A Reply Queue - -When using a fixed reply queue (other than `amq.rabbitmq.reply-to`), you must provide correlation data so that replies can be correlated to requests. -See https://www.rabbitmq.com/tutorials/tutorial-six-java.html[RabbitMQ Remote Procedure Call (RPC)]. -By default, the standard `correlationId` property is used to hold the correlation data. -However, if you wish to use a custom property to hold correlation data, you can set the `correlation-key` attribute on the . -Explicitly setting the attribute to `correlationId` is the same as omitting the attribute. -The client and server must use the same header for correlation data. - -NOTE: Spring AMQP version 1.1 used a custom property called `spring_reply_correlation` for this data. -If you wish to revert to this behavior with the current version (perhaps to maintain compatibility with another application using 1.1), you must set the attribute to `spring_reply_correlation`. - -By default, the template generates its own correlation ID (ignoring any user-supplied value). -If you wish to use your own correlation ID, set the `RabbitTemplate` instance's `userCorrelationId` property to `true`. - -IMPORTANT: The correlation ID must be unique to avoid the possibility of a wrong reply being returned for a request. - -[[reply-listener]] -===== Reply Listener Container - -When using RabbitMQ versions prior to 3.4.0, a new temporary queue is used for each reply. -However, a single reply queue can be configured on the template, which can be more efficient and also lets you set arguments on that queue. -In this case, however, you must also provide a sub element. -This element provides a listener container for the reply queue, with the template being the listener. -All of the <> attributes allowed on a are allowed on the element, except for `connection-factory` and `message-converter`, which are inherited from the template's configuration. - -IMPORTANT: If you run multiple instances of your application or use multiple `RabbitTemplate` instances, you *MUST* use a unique reply queue for each. -RabbitMQ has no ability to select messages from a queue, so, if they all use the same queue, each instance would compete for replies and not necessarily receive their own. - -The following example defines a rabbit template with a connection factory: - -==== -[source,xml] ----- - - - ----- -==== - -While the container and template share a connection factory, they do not share a channel. -Therefore, requests and replies are not performed within the same transaction (if transactional). - -NOTE: Prior to version 1.5.0, the `reply-address` attribute was not available. -Replies were always routed by using the default exchange and the `reply-queue` name as the routing key. -This is still the default, but you can now specify the new `reply-address` attribute. -The `reply-address` can contain an address with the form `/` and the reply is routed to the specified exchange and routed to a queue bound with the routing key. -The `reply-address` has precedence over `reply-queue`. -When only `reply-address` is in use, the `` must be configured as a separate `` component. -The `reply-address` and `reply-queue` (or `queues` attribute on the ``) must refer to the same queue logically. - -With this configuration, a `SimpleListenerContainer` is used to receive the replies, with the `RabbitTemplate` being the `MessageListener`. -When defining a template with the `` namespace element, as shown in the preceding example, the parser defines the container and wires in the template as the listener. - -NOTE: When the template does not use a fixed `replyQueue` (or is using direct reply-to -- see <>), a listener container is not needed. -Direct `reply-to` is the preferred mechanism when using RabbitMQ 3.4.0 or later. - -If you define your `RabbitTemplate` as a `` or use an `@Configuration` class to define it as an `@Bean` or when you create the template programmatically, you need to define and wire up the reply listener container yourself. -If you fail to do this, the template never receives the replies and eventually times out and returns null as the reply to a call to a `sendAndReceive` method. - -Starting with version 1.5, the `RabbitTemplate` detects if it has been -configured as a `MessageListener` to receive replies. -If not, attempts to send and receive messages with a reply address -fail with an `IllegalStateException` (because the replies are never received). - -Further, if a simple `replyAddress` (queue name) is used, the reply listener container verifies that it is listening -to a queue with the same name. -This check cannot be performed if the reply address is an exchange and routing key and a debug log message is written. - -IMPORTANT: When wiring the reply listener and template yourself, it is important to ensure that the template's `replyAddress` and the container's `queues` (or `queueNames`) properties refer to the same queue. -The template inserts the reply address into the outbound message `replyTo` property. - -The following listing shows examples of how to manually wire up the beans: - -==== -[source,xml] ----- - - - - - - - - - - - - - - - - ----- - -[source,java] ----- - @Bean - public RabbitTemplate amqpTemplate() { - RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory()); - rabbitTemplate.setMessageConverter(msgConv()); - rabbitTemplate.setReplyAddress(replyQueue().getName()); - rabbitTemplate.setReplyTimeout(60000); - rabbitTemplate.setUseDirectReplyToContainer(false); - return rabbitTemplate; - } - - @Bean - public SimpleMessageListenerContainer replyListenerContainer() { - SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); - container.setConnectionFactory(connectionFactory()); - container.setQueues(replyQueue()); - container.setMessageListener(amqpTemplate()); - return container; - } - - @Bean - public Queue replyQueue() { - return new Queue("my.reply.queue"); - } ----- -==== - -A complete example of a `RabbitTemplate` wired with a fixed reply queue, together with a "`remote`" listener container that handles the request and returns the reply is shown in https://github.com/spring-projects/spring-amqp/tree/main/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/JavaConfigFixedReplyQueueTests.java[this test case]. - -IMPORTANT: When the reply times out (`replyTimeout`), the `sendAndReceive()` methods return null. - -Prior to version 1.3.6, late replies for timed out messages were only logged. -Now, if a late reply is received, it is rejected (the template throws an `AmqpRejectAndDontRequeueException`). -If the reply queue is configured to send rejected messages to a dead letter exchange, the reply can be retrieved for later analysis. -To do so, bind a queue to the configured dead letter exchange with a routing key equal to the reply queue's name. - -See the https://www.rabbitmq.com/dlx.html[RabbitMQ Dead Letter Documentation] for more information about configuring dead lettering. -You can also take a look at the `FixedReplyQueueDeadLetterTests` test case for an example. - -[[async-template]] -===== Async Rabbit Template - -Version 1.6 introduced the `AsyncRabbitTemplate`. -This has similar `sendAndReceive` (and `convertSendAndReceive`) methods to those on the <>. -However, instead of blocking, they return a `CompletableFuture`. - -The `sendAndReceive` methods return a `RabbitMessageFuture`. -The `convertSendAndReceive` methods return a `RabbitConverterFuture`. - -You can either synchronously retrieve the result later, by invoking `get()` on the future, or you can register a callback that is called asynchronously with the result. -The following listing shows both approaches: - -==== -[source, java] ----- -@Autowired -private AsyncRabbitTemplate template; - -... - -public void doSomeWorkAndGetResultLater() { - - ... - - CompletableFuture future = this.template.convertSendAndReceive("foo"); - - // do some more work - - String reply = null; - try { - reply = future.get(10, TimeUnit.SECONDS); - } - catch (ExecutionException e) { - ... - } - - ... - -} - -public void doSomeWorkAndGetResultAsync() { - - ... - - RabbitConverterFuture future = this.template.convertSendAndReceive("foo"); - future.whenComplete((result, ex) -> { - if (ex == null) { - // success - } - else { - // failure - } - }); - - ... - -} ----- -==== - -If `mandatory` is set and the message cannot be delivered, the future throws an `ExecutionException` with a cause of `AmqpMessageReturnedException`, which encapsulates the returned message and information about the return. - -If `enableConfirms` is set, the future has a property called `confirm`, which is itself a `CompletableFuture` with `true` indicating a successful publish. -If the confirm future is `false`, the `RabbitFuture` has a further property called `nackCause`, which contains the reason for the failure, if available. - -IMPORTANT: The publisher confirm is discarded if it is received after the reply, since the reply implies a successful publish. - -You can set the `receiveTimeout` property on the template to time out replies (it defaults to `30000` - 30 seconds). -If a timeout occurs, the future is completed with an `AmqpReplyTimeoutException`. - -The template implements `SmartLifecycle`. -Stopping the template while there are pending replies causes the pending `Future` instances to be canceled. - -Starting with version 2.0, the asynchronous template now supports https://www.rabbitmq.com/direct-reply-to.html[direct reply-to] instead of a configured reply queue. -To enable this feature, use one of the following constructors: - -==== -[source, java] ----- -public AsyncRabbitTemplate(ConnectionFactory connectionFactory, String exchange, String routingKey) - -public AsyncRabbitTemplate(RabbitTemplate template) ----- -==== - -See <> to use direct reply-to with the synchronous `RabbitTemplate`. - -Version 2.0 introduced variants of these methods (`convertSendAndReceiveAsType`) that take an additional `ParameterizedTypeReference` argument to convert complex returned types. -You must configure the underlying `RabbitTemplate` with a `SmartMessageConverter`. -See <> for more information. - -IMPORTANT: Starting with version 3.0, the `AsyncRabbitTemplate` methods now return `CompletableFuture` s instead of `ListenableFuture` s. - -[[remoting]] -===== Spring Remoting with AMQP - -Spring remoting is no longer supported because the functionality has been removed from Spring Framework. - -Use `sendAndReceive` operations using the `RabbitTemplate` (client side ) and `@RabbitListener` instead. - -[[broker-configuration]] -==== Configuring the Broker - -The AMQP specification describes how the protocol can be used to configure queues, exchanges, and bindings on the broker. -These operations (which are portable from the 0.8 specification and higher) are present in the `AmqpAdmin` interface in the `org.springframework.amqp.core` package. -The RabbitMQ implementation of that class is `RabbitAdmin` located in the `org.springframework.amqp.rabbit.core` package. - -The `AmqpAdmin` interface is based on using the Spring AMQP domain abstractions and is shown in the following listing: - -==== -[source,java] ----- -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); - -} ----- -==== - -See also <>. - -The `getQueueProperties()` method returns some limited information about the queue (message count and consumer count). -The keys for the properties returned are available as constants in the `RabbitTemplate` (`QUEUE_NAME`, -`QUEUE_MESSAGE_COUNT`, and `QUEUE_CONSUMER_COUNT`). -The <> provides much more information in the `QueueInfo` object. - -The no-arg `declareQueue()` method defines a queue on the broker with a name that is automatically generated. -The additional properties of this auto-generated queue are `exclusive=true`, `autoDelete=true`, and `durable=false`. - -The `declareQueue(Queue queue)` method takes a `Queue` object and returns the name of the declared queue. -If the `name` property of the provided `Queue` is an empty `String`, the broker declares the queue with a generated name. -That name is returned to the caller. -That name is also added to the `actualName` property of the `Queue`. -You can use this functionality programmatically only by invoking the `RabbitAdmin` directly. -When using auto-declaration by the admin when defining a queue declaratively in the application context, you can set the name property to `""` (the empty string). -The broker then creates the name. -Starting with version 2.1, listener containers can use queues of this type. -See <> for more information. - -This is in contrast to an `AnonymousQueue` where the framework generates a unique (`UUID`) name and sets `durable` to -`false` and `exclusive`, `autoDelete` to `true`. -A `` with an empty (or missing) `name` attribute always creates an `AnonymousQueue`. - -See <> to understand why `AnonymousQueue` is preferred over broker-generated queue names as well as -how to control the format of the name. -Starting with version 2.1, anonymous queues are declared with argument `Queue.X_QUEUE_LEADER_LOCATOR` set to `client-local` by default. -This ensures that the queue is declared on the node to which the application is connected. -Declarative queues must have fixed names because they might be referenced elsewhere in the context -- such as in the -listener shown in the following example: - -==== -[source,xml] ----- - - - ----- -==== - -See <>. - -The RabbitMQ implementation of this interface is `RabbitAdmin`, which, when configured by using Spring XML, resembles the following example: - -==== -[source,xml] ----- - - - ----- -==== - -When the `CachingConnectionFactory` cache mode is `CHANNEL` (the default), the `RabbitAdmin` implementation does automatic lazy declaration of queues, exchanges, and bindings declared in the same `ApplicationContext`. -These components are declared as soon as a `Connection` is opened to the broker. -There are some namespace features that make this very convenient -- for example, -in the Stocks sample application, we have the following: - -==== -[source,xml] ----- - - - - - - - - - - - - - - - ----- -==== - -In the preceding example, we use anonymous queues (actually, internally, just queues with names generated by the framework, not by the broker) and refer to them by ID. -We can also declare queues with explicit names, which also serve as identifiers for their bean definitions in the context. -The following example configures a queue with an explicit name: - -==== -[source,xml] ----- - ----- -==== - -TIP: You can provide both `id` and `name` attributes. -This lets you refer to the queue (for example, in a binding) by an ID that is independent of the queue name. -It also allows standard Spring features (such as property placeholders and SpEL expressions for the queue name). -These features are not available when you use the name as the bean identifier. - -Queues can be configured with additional arguments -- for example, `x-message-ttl`. -When you use the namespace support, they are provided in the form of a `Map` of argument-name/argument-value pairs, which are defined by using the `` element. -The following example shows how to do so: - -==== -[source,xml] ----- - - - - - - ----- -==== - -By default, the arguments are assumed to be strings. -For arguments of other types, you must provide the type. -The following example shows how to specify the type: - -==== -[source,xml] ----- - - - - - ----- -==== - -When providing arguments of mixed types, you must provide the type for each entry element. -The following example shows how to do so: - -==== -[source,xml] ----- - - - - 100 - - - - - ----- -==== - -With Spring Framework 3.2 and later, this can be declared a little more succinctly, as follows: - -==== -[source,xml] ----- - - - - - - ----- -==== - -When you use Java configuration, the `Queue.X_QUEUE_LEADER_LOCATOR` argument is supported as a first class property through the `setLeaderLocator()` method on the `Queue` class. -Starting with version 2.1, anonymous queues are declared with this property set to `client-local` by default. -This ensures that the queue is declared on the node the application is connected to. - -IMPORTANT: The RabbitMQ broker does not allow declaration of a queue with mismatched arguments. -For example, if a `queue` already exists with no `time to live` argument, and you attempt to declare it with (for example) `key="x-message-ttl" value="100"`, an exception is thrown. - -By default, the `RabbitAdmin` immediately stops processing all declarations when any exception occurs. -This could cause downstream issues, such as a listener container failing to initialize because another queue (defined after the one in error) is not declared. - -This behavior can be modified by setting the `ignore-declaration-exceptions` attribute to `true` on the `RabbitAdmin` instance. -This option instructs the `RabbitAdmin` to log the exception and continue declaring other elements. -When configuring the `RabbitAdmin` using Java, this property is called `ignoreDeclarationExceptions`. -This is a global setting that applies to all elements. -Queues, exchanges, and bindings have a similar property that applies to just those elements. - -Prior to version 1.6, this property took effect only if an `IOException` occurred on the channel, such as when there is a mismatch between current and desired properties. -Now, this property takes effect on any exception, including `TimeoutException` and others. - -In addition, any declaration exceptions result in the publishing of a `DeclarationExceptionEvent`, which is an `ApplicationEvent` that can be consumed by any `ApplicationListener` in the context. -The event contains a reference to the admin, the element that was being declared, and the `Throwable`. - -[[headers-exchange]] -===== Headers Exchange - -Starting with version 1.3, you can configure the `HeadersExchange` to match on multiple headers. -You can also specify whether any or all headers must match. -The following example shows how to do so: - -==== -[source,xml] ----- - - - - - - - - - - - ----- -==== - -Starting with version 1.6, you can configure `Exchanges` with an `internal` flag (defaults to `false`) and such an -`Exchange` is properly configured on the Broker through a `RabbitAdmin` (if one is present in the application context). -If the `internal` flag is `true` for an exchange, RabbitMQ does not let clients use the exchange. -This is useful for a dead letter exchange or exchange-to-exchange binding, where you do not wish the exchange to be used -directly by publishers. - -To see how to use Java to configure the AMQP infrastructure, look at the Stock sample application, -where there is the `@Configuration` class `AbstractStockRabbitConfiguration`, which ,in turn has -`RabbitClientConfiguration` and `RabbitServerConfiguration` subclasses. -The following listing shows the code for `AbstractStockRabbitConfiguration`: - -==== -[source,java] ----- -@Configuration -public abstract class AbstractStockAppRabbitConfiguration { - - @Bean - public CachingConnectionFactory 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 Jackson2JsonMessageConverter jsonMessageConverter() { - return new Jackson2JsonMessageConverter(); - } - - @Bean - public TopicExchange marketDataExchange() { - return new TopicExchange("app.stock.marketdata"); - } - - // additional code omitted for brevity - -} ----- -==== - -In the Stock application, the server is configured by using the following `@Configuration` class: - -==== -[source,java] ----- -@Configuration -public class RabbitServerConfiguration extends AbstractStockAppRabbitConfiguration { - - @Bean - public Queue stockRequestQueue() { - return new Queue("app.stock.request"); - } -} ----- -==== - -This is the end of the whole inheritance chain of `@Configuration` classes. -The end result is that `TopicExchange` and `Queue` are declared to the broker upon application startup. -There is no binding of `TopicExchange` to a queue in the server configuration, as that is done in the client application. -The stock request queue, however, is automatically bound to the AMQP default exchange. -This behavior is defined by the specification. - -The client `@Configuration` class is a little more interesting. -Its declaration follows: - -==== -[source,java] ----- -@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 - -} ----- -==== - -The client declares another queue through the `declareQueue()` method on the `AmqpAdmin`. -It binds that queue to the market data exchange with a routing pattern that is externalized in a properties file. - - -[[builder-api]] -===== Builder API for Queues and Exchanges - -Version 1.6 introduces a convenient fluent API for configuring `Queue` and `Exchange` objects when using Java configuration. -The following example shows how to use it: - -==== -[source, 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 Javadoc for https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/core/QueueBuilder.html[`org.springframework.amqp.core.QueueBuilder`] and https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/core/ExchangeBuilder.html[`org.springframework.amqp.core.ExchangeBuilder`] for more information. - -Starting with version 2.0, the `ExchangeBuilder` now creates durable exchanges by default, to be consistent with the simple constructors on the individual `AbstractExchange` classes. -To make a non-durable exchange with the builder, use `.durable(false)` before invoking `.build()`. -The `durable()` method with no parameter is no longer provided. - -Version 2.2 introduced fluent APIs to add "well known" exchange and queue arguments... - -==== -[source, java] ----- -@Bean -public Queue allArgs1() { - return QueueBuilder.nonDurable("all.args.1") - .ttl(1000) - .expires(200_000) - .maxLength(42) - .maxLengthBytes(10_000) - .overflow(Overflow.rejectPublish) - .deadLetterExchange("dlx") - .deadLetterRoutingKey("dlrk") - .maxPriority(4) - .lazy() - .leaderLocator(LeaderLocator.minLeaders) - .singleActiveConsumer() - .build(); -} - -@Bean -public DirectExchange ex() { - return ExchangeBuilder.directExchange("ex.with.alternate") - .durable(true) - .alternate("alternate") - .build(); -} ----- -==== - -[[collection-declaration]] -===== Declaring Collections of Exchanges, Queues, and Bindings - -You can wrap collections of `Declarable` objects (`Queue`, `Exchange`, and `Binding`) in `Declarables` objects. -The `RabbitAdmin` detects such beans (as well as discrete `Declarable` beans) in the application context, and declares the contained objects on the broker whenever a connection is established (initially and after a connection failure). -The following example shows how to do so: - -==== -[source, java] ----- -@Configuration -public static class Config { - - @Bean - public CachingConnectionFactory 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 Declarables es() { - return new Declarables( - new DirectExchange("e2", false, true), - new DirectExchange("e3", false, true)); - } - - @Bean - public Declarables qs() { - return new Declarables( - new Queue("q2", false, false, true), - new Queue("q3", false, false, true)); - } - - @Bean - @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) - public Declarables prototypes() { - return new Declarables(new Queue(this.prototypeQueueName, false, false, true)); - } - - @Bean - public Declarables bs() { - return new Declarables( - new Binding("q2", DestinationType.QUEUE, "e2", "k2", null), - new Binding("q3", DestinationType.QUEUE, "e3", "k3", null)); - } - - @Bean - public Declarables ds() { - return new Declarables( - new DirectExchange("e4", false, true), - new Queue("q4", false, false, true), - new Binding("q4", DestinationType.QUEUE, "e4", "k4", null)); - } - -} ----- -==== - -IMPORTANT: In versions prior to 2.1, you could declare multiple `Declarable` instances by defining beans of type `Collection`. -This can cause undesirable side effects in some cases, because the admin has to iterate over all `Collection` beans. - -Version 2.2 added the `getDeclarablesByType` method to `Declarables`; this can be used as a convenience, for example, when declaring the listener container bean(s). - -==== -[source, java] ----- -public SimpleMessageListenerContainer container(ConnectionFactory connectionFactory, - Declarables mixedDeclarables, MessageListener listener) { - - SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); - container.setQueues(mixedDeclarables.getDeclarablesByType(Queue.class).toArray(new Queue[0])); - container.setMessageListener(listener); - return container; -} ----- -==== - -[[conditional-declaration]] -===== Conditional Declaration - -By default, all queues, exchanges, and bindings are declared by all `RabbitAdmin` instances (assuming they have `auto-startup="true"`) in the application context. - -Starting with version 2.1.9, the `RabbitAdmin` has a new property `explicitDeclarationsOnly` (which is `false` by default); when this is set to `true`, the admin will only declare beans that are explicitly configured to be declared by that admin. - -NOTE: Starting with the 1.2 release, you can conditionally declare these elements. -This is particularly useful when an application connects to multiple brokers and needs to specify with which brokers a particular element should be declared. - -The classes representing these elements implement `Declarable`, which has two methods: `shouldDeclare()` and `getDeclaringAdmins()`. -The `RabbitAdmin` uses these methods to determine whether a particular instance should actually process the declarations on its `Connection`. - -The properties are available as attributes in the namespace, as shown in the following examples: - -==== -[source,xml] ----- - - - - - - - - - - - - - - - - - - - ----- -==== - -NOTE: By default, the `auto-declare` attribute is `true` and, if the `declared-by` is not supplied (or is empty), then all `RabbitAdmin` instances declare the object (as long as the admin's `auto-startup` attribute is `true`, the default, and the admin's `explicit-declarations-only` attribute is false). - -Similarly, you can use Java-based `@Configuration` to achieve the same effect. -In the following example, the components are declared by `admin1` but not by `admin2`: - -==== -[source,java] ----- -@Bean -public RabbitAdmin admin1() { - return new RabbitAdmin(cf1()); -} - -@Bean -public RabbitAdmin admin2() { - return new RabbitAdmin(cf2()); -} - -@Bean -public Queue queue() { - Queue queue = new Queue("foo"); - queue.setAdminsThatShouldDeclare(admin1()); - return queue; -} - -@Bean -public Exchange exchange() { - DirectExchange exchange = new DirectExchange("bar"); - exchange.setAdminsThatShouldDeclare(admin1()); - return exchange; -} - -@Bean -public Binding binding() { - Binding binding = new Binding("foo", DestinationType.QUEUE, exchange().getName(), "foo", null); - binding.setAdminsThatShouldDeclare(admin1()); - return binding; -} ----- -==== - -[[note-id-name]] -===== A Note On the `id` and `name` Attributes - -The `name` attribute on `` and `` elements reflects the name of the entity in the broker. -For queues, if the `name` is omitted, an anonymous queue is created (see <>). - -In versions prior to 2.0, the `name` was also registered as a bean name alias (similar to `name` on `` elements). - -This caused two problems: - -* It prevented the declaration of a queue and exchange with the same name. -* The alias was not resolved if it contained a SpEL expression (`#{...}`). - -Starting with version 2.0, if you declare one of these elements with both an `id` _and_ a `name` attribute, the name is no longer declared as a bean name alias. -If you wish to declare a queue and exchange with the same `name`, you must provide an `id`. - -There is no change if the element has only a `name` attribute. -The bean can still be referenced by the `name` -- for example, in binding declarations. -However, you still cannot reference it if the name contains SpEL -- you must provide an `id` for reference purposes. - - -[[anonymous-queue]] -===== `AnonymousQueue` - -In general, when you need a uniquely-named, exclusive, auto-delete queue, we recommend that you use the `AnonymousQueue` -instead of broker-defined queue names (using `""` as a `Queue` name causes the broker to generate the queue -name). - -This is because: - -. The queues are actually declared when the connection to the broker is established. -This is long after the beans are created and wired together. -Beans that use the queue need to know its name. -In fact, the broker might not even be running when the application is started. -. If the connection to the broker is lost for some reason, the admin re-declares the `AnonymousQueue` with the same name. -If we used broker-declared queues, the queue name would change. - -You can control the format of the queue name used by `AnonymousQueue` instances. - -By default, the queue name is prefixed by `spring.gen-` followed by a base64 representation of the `UUID` -- for example: `spring.gen-MRBv9sqISkuCiPfOYfpo4g`. - -You can provide an `AnonymousQueue.NamingStrategy` implementation in a constructor argument. -The following example shows how to do so: - -==== -[source, java] ----- -@Bean -public Queue anon1() { - return new AnonymousQueue(); -} - -@Bean -public Queue anon2() { - return new AnonymousQueue(new AnonymousQueue.Base64UrlNamingStrategy("something-")); -} - -@Bean -public Queue anon3() { - return new AnonymousQueue(AnonymousQueue.UUIDNamingStrategy.DEFAULT); -} ----- -==== - -The first bean generates a queue name prefixed by `spring.gen-` followed by a base64 representation of the `UUID` -- for -example: `spring.gen-MRBv9sqISkuCiPfOYfpo4g`. -The second bean generates a queue name prefixed by `something-` followed by a base64 representation of the `UUID`. -The third bean generates a name by using only the UUID (no base64 conversion) -- for example, `f20c818a-006b-4416-bf91-643590fedb0e`. - -The base64 encoding uses the "`URL and Filename Safe Alphabet`" from RFC 4648. -Trailing padding characters (`=`) are removed. - -You can provide your own naming strategy, whereby you can include other information (such as the application name or client host) in the queue name. - -You can specify the naming strategy when you use XML configuration. -The `naming-strategy` attribute is present on the `` element -for a bean reference that implements `AnonymousQueue.NamingStrategy`. -The following examples show how to specify the naming strategy in various ways: - -==== -[source, xml] ----- - - - - - - - - - - - ----- -==== - -The first example creates names such as `spring.gen-MRBv9sqISkuCiPfOYfpo4g`. -The second example creates names with a String representation of a UUID. -The third example creates names such as `custom.gen-MRBv9sqISkuCiPfOYfpo4g`. - -You can also provide your own naming strategy bean. - -Starting with version 2.1, anonymous queues are declared with argument `Queue.X_QUEUE_LEADER_LOCATOR` set to `client-local` by default. -This ensures that the queue is declared on the node to which the application is connected. -You can revert to the previous behavior by calling `queue.setLeaderLocator(null)` after constructing the instance. - -[[declarable-recovery]] -===== Recovering Auto-Delete Declarations - -Normally, the `RabbitAdmin` (s) only recover queues/exchanges/bindings that are declared as beans in the application context; if any such declarations are auto-delete, they will be removed by the broker if the connection is lost. -When the connection is re-established, the admin will redeclare the entities. -Normally, entities created by calling `admin.declareQueue(...)`, `admin.declareExchange(...)` and `admin.declareBinding(...)` will not be recovered. - -Starting with version 2.4, the admin has a new property `redeclareManualDeclarations`; when `true`, the admin will recover these entities in addition to the beans in the application context. - -Recovery of individual declarations will not be performed if `deleteQueue(...)`, `deleteExchange(...)` or `removeBinding(...)` is called. -Associated bindings are removed from the recoverable entities when queues and exchanges are deleted. - -Finally, calling `resetAllManualDeclarations()` will prevent the recovery of any previously declared entities. - -[[broker-events]] -==== Broker Event Listener - -When the https://www.rabbitmq.com/event-exchange.html[Event Exchange Plugin] is enabled, if you add a bean of type `BrokerEventListener` to the application context, it publishes selected broker events as `BrokerEvent` instances, which can be consumed with a normal Spring `ApplicationListener` or `@EventListener` method. -Events are published by the broker to a topic exchange `amq.rabbitmq.event` with a different routing key for each event type. -The listener uses event keys, which are used to bind an `AnonymousQueue` to the exchange so the listener receives only selected events. -Since it is a topic exchange, wildcards can be used (as well as explicitly requesting specific events), as the following example shows: - -==== -[source, java] ----- -@Bean -public BrokerEventListener eventListener() { - return new BrokerEventListener(connectionFactory(), "user.deleted", "channel.#", "queue.#"); -} ----- -==== - -You can further narrow the received events in individual event listeners, by using normal Spring techniques, as the following example shows: - -==== -[source, java] ----- -@EventListener(condition = "event.eventType == 'queue.created'") -public void listener(BrokerEvent event) { - ... -} ----- -==== - -[[delayed-message-exchange]] -==== Delayed Message Exchange - -Version 1.6 introduces support for the -https://www.rabbitmq.com/blog/2015/04/16/scheduling-messages-with-rabbitmq/[Delayed Message Exchange Plugin] - -NOTE: The plugin is currently marked as experimental but has been available for over a year (at the time of writing). -If changes to the plugin make it necessary, we plan to add support for such changes as soon as practical. -For that reason, this support in Spring AMQP should be considered experimental, too. -This functionality was tested with RabbitMQ 3.6.0 and version 0.0.1 of the plugin. - -To use a `RabbitAdmin` to declare an exchange as delayed, you can set the `delayed` property on the exchange bean to -`true`. -The `RabbitAdmin` uses the exchange type (`Direct`, `Fanout`, and so on) to set the `x-delayed-type` argument and -declare the exchange with type `x-delayed-message`. - -The `delayed` property (default: `false`) is also available when configuring exchange beans using XML. -The following example shows how to use it: - -==== -[source, xml] ----- - ----- -==== - -To send a delayed message, you can set the `x-delay` header through `MessageProperties`, as the following examples show: - -==== -[source, java] ----- -MessageProperties properties = new MessageProperties(); -properties.setDelay(15000); -template.send(exchange, routingKey, - MessageBuilder.withBody("foo".getBytes()).andProperties(properties).build()); ----- - -[source, java] ----- -rabbitTemplate.convertAndSend(exchange, routingKey, "foo", new MessagePostProcessor() { - - @Override - public Message postProcessMessage(Message message) throws AmqpException { - message.getMessageProperties().setDelay(15000); - return message; - } - -}); ----- -==== - -To check if a message was delayed, use the `getReceivedDelay()` method on the `MessageProperties`. -It is a separate property to avoid unintended propagation to an output message generated from an input message. - - -[[management-rest-api]] -==== RabbitMQ REST API - -When the management plugin is enabled, the RabbitMQ server exposes a REST API to monitor and configure the broker. -A https://github.com/rabbitmq/hop[Java Binding for the API] is now provided. -The `com.rabbitmq.http.client.Client` is a standard, immediate, and, therefore, blocking API. -It is based on the https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#spring-web[Spring Web] module and its `RestTemplate` implementation. -On the other hand, the `com.rabbitmq.http.client.ReactorNettyClient` is a reactive, non-blocking implementation based on the https://projectreactor.io/docs/netty/release/reference/docs/index.html[Reactor Netty] project. - -The hop dependency (`com.rabbitmq:http-client`) is now also `optional`. - -See their Javadoc for more information. - -[[exception-handling]] -==== Exception Handling - -Many operations with the RabbitMQ Java client can throw checked exceptions. -For example, there are a lot of cases where `IOException` instances may be thrown. -The `RabbitTemplate`, `SimpleMessageListenerContainer`, and other Spring AMQP components catch those exceptions and convert them into one of the exceptions within `AmqpException` hierarchy. -Those are defined in the 'org.springframework.amqp' package, and `AmqpException` is the base of the hierarchy. - -When a listener throws an exception, it is wrapped in a `ListenerExecutionFailedException`. -Normally the message is rejected and requeued by the broker. -Setting `defaultRequeueRejected` to `false` causes messages to be discarded (or routed to a dead letter exchange). -As discussed in <>, the listener can throw an `AmqpRejectAndDontRequeueException` (or `ImmediateRequeueAmqpException`) to conditionally control this behavior. - -However, there is a class of errors where the listener cannot control the behavior. -When a message that cannot be converted is encountered (for example, an invalid `content_encoding` header), some exceptions are thrown before the message reaches user code. -With `defaultRequeueRejected` set to `true` (default) (or throwing an `ImmediateRequeueAmqpException`), such messages would be redelivered over and over. -Before version 1.3.2, users needed to write a custom `ErrorHandler`, as discussed in <>, to avoid this situation. - -Starting with version 1.3.2, the default `ErrorHandler` is now a `ConditionalRejectingErrorHandler` that rejects (and does not requeue) messages that fail with an irrecoverable error. -Specifically, it rejects messages that fail with the following errors: - -* `o.s.amqp...MessageConversionException`: Can be thrown when converting the incoming message payload using a `MessageConverter`. -* `o.s.messaging...MessageConversionException`: Can be thrown by the conversion service if additional conversion is required when mapping to a `@RabbitListener` method. -* `o.s.messaging...MethodArgumentNotValidException`: Can be thrown if validation (for example, `@Valid`) is used in the listener and the validation fails. -* `o.s.messaging...MethodArgumentTypeMismatchException`: Can be thrown if the inbound message was converted to a type that is not correct for the target method. -For example, the parameter is declared as `Message` but `Message` is received. -* `java.lang.NoSuchMethodException`: Added in version 1.6.3. -* `java.lang.ClassCastException`: Added in version 1.6.3. - -You can configure an instance of this error handler with a `FatalExceptionStrategy` so that users can provide their own rules for conditional message rejection -- for example, a delegate implementation to the `BinaryExceptionClassifier` from Spring Retry (<>). -In addition, the `ListenerExecutionFailedException` now has a `failedMessage` property that you can use in the decision. -If the `FatalExceptionStrategy.isFatal()` method returns `true`, the error handler throws an `AmqpRejectAndDontRequeueException`. -The default `FatalExceptionStrategy` logs a warning message when an exception is determined to be fatal. - -Since version 1.6.3, a convenient way to add user exceptions to the fatal list is to subclass `ConditionalRejectingErrorHandler.DefaultExceptionStrategy` and override the `isUserCauseFatal(Throwable cause)` method to return `true` for fatal exceptions. - -A common pattern for handling DLQ messages is to set a `time-to-live` on those messages as well as additional DLQ configuration such that these messages expire and are routed back to the main queue for retry. -The problem with this technique is that messages that cause fatal exceptions loop forever. -Starting with version 2.1, the `ConditionalRejectingErrorHandler` detects an `x-death` header on a message that causes a fatal exception to be thrown. -The message is logged and discarded. -You can revert to the previous behavior by setting the `discardFatalsWithXDeath` property on the `ConditionalRejectingErrorHandler` to `false`. - -IMPORTANT: Starting with version 2.1.9, messages with these fatal exceptions are rejected and NOT requeued by default, even if the container acknowledge mode is MANUAL. -These exceptions generally occur before the listener is invoked so the listener does not have a chance to ack or nack the message so it remained in the queue in an un-acked state. -To revert to the previous behavior, set the `rejectManual` property on the `ConditionalRejectingErrorHandler` to `false`. - -[[transactions]] -==== Transactions - -The Spring Rabbit framework has support for automatic transaction management in the synchronous and asynchronous use cases with a number of different semantics that can be selected declaratively, as is familiar to existing users of Spring transactions. -This makes many if not most common messaging patterns easy to implement. - -There are two ways to signal the desired transaction semantics to the framework. -In both the `RabbitTemplate` and `SimpleMessageListenerContainer`, there is a flag `channelTransacted` which, if `true`, tells the framework to use a transactional channel and to end all operations (send or receive) with a commit or rollback (depending on the outcome), with an exception signaling a rollback. -Another signal is to provide an external transaction with one of Spring's `PlatformTransactionManager` implementations as a context for the ongoing operation. -If there is already a transaction in progress when the framework is sending or receiving a message, and the `channelTransacted` flag is `true`, the commit or rollback of the messaging transaction is deferred until the end of the current transaction. -If the `channelTransacted` flag is `false`, no transaction semantics apply to the messaging operation (it is auto-acked). - -The `channelTransacted` flag is a configuration time setting. -It is declared and processed once when the AMQP components are created, usually at application startup. -The external transaction is more dynamic in principle because the system responds to the current thread state at runtime. -However, in practice, it is often also a configuration setting, when the transactions are layered onto an application declaratively. - -For synchronous use cases with `RabbitTemplate`, the external transaction is provided by the caller, either declaratively or imperatively according to taste (the usual Spring transaction model). -The following example shows a declarative approach (usually preferred because it is non-invasive), where the template has been configured with `channelTransacted=true`: - -==== -[source,java] ----- -@Transactional -public void doSomething() { - String incoming = rabbitTemplate.receiveAndConvert(); - // do some more database processing... - String outgoing = processInDatabaseAndExtractReply(incoming); - rabbitTemplate.convertAndSend(outgoing); -} ----- -==== - -In the preceding example, a `String` payload is received, converted, and sent as a message body inside a method marked as `@Transactional`. -If the database processing fails with an exception, the incoming message is returned to the broker, and the outgoing message is not sent. -This applies to any operations with the `RabbitTemplate` inside a chain of transactional methods (unless, for instance, the `Channel` is directly manipulated to commit the transaction early). - -For asynchronous use cases with `SimpleMessageListenerContainer`, if an external transaction is needed, it has to be requested by the container when it sets up the listener. -To signal that an external transaction is required, the user provides an implementation of `PlatformTransactionManager` to the container when it is configured. -The following example shows how to do so: - -==== -[source,java] ----- -@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; - } - -} ----- -==== - -In the preceding example, the transaction manager is added as a dependency injected from another bean definition (not shown), and the `channelTransacted` flag is also set to `true`. -The effect is that if the listener fails with an exception, the transaction is rolled back, and the message is also returned to the broker. -Significantly, if the transaction fails to commit (for example, because of -a database constraint error or connectivity problem), the AMQP transaction is also rolled back, and the message is returned to the broker. -This is sometimes known as a "`Best Efforts 1 Phase Commit`", and is a very powerful pattern for reliable messaging. -If the `channelTransacted` flag was set to `false` (the default) in the preceding example, the external transaction would still be provided for the listener, but all messaging operations would be auto-acked, so the effect is to commit the messaging operations even on a rollback of the business operation. - -[[conditional-rollback]] -===== Conditional Rollback - -Prior to version 1.6.6, adding a rollback rule to a container's `transactionAttribute` when using an external transaction manager (such as JDBC) had no effect. -Exceptions always rolled back the transaction. - -Also, when using a https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/html/transaction.html#transaction-declarative[transaction advice] in the container's advice chain, conditional rollback was not very useful, because all listener exceptions are wrapped in a `ListenerExecutionFailedException`. - -The first problem has been corrected, and the rules are now applied properly. -Further, the `ListenerFailedRuleBasedTransactionAttribute` is now provided. -It is a subclass of `RuleBasedTransactionAttribute`, with the only difference being that it is aware of the `ListenerExecutionFailedException` and uses the cause of such exceptions for the rule. -This transaction attribute can be used directly in the container or through a transaction advice. - -The following example uses this rule: - -==== -[source, java] ----- -@Bean -public AbstractMessageListenerContainer container() { - ... - container.setTransactionManager(transactionManager); - RuleBasedTransactionAttribute transactionAttribute = - new ListenerFailedRuleBasedTransactionAttribute(); - transactionAttribute.setRollbackRules(Collections.singletonList( - new NoRollbackRuleAttribute(DontRollBackException.class))); - container.setTransactionAttribute(transactionAttribute); - ... -} ----- -==== - -[[transaction-rollback]] -===== A note on Rollback of Received Messages - -AMQP transactions apply only to messages and acks sent to the broker. -Consequently, when there is a rollback of a Spring transaction and a message has been received, Spring AMQP has to not only rollback the transaction but also manually reject the message (sort of a nack, but that is not what the specification calls it). -The action taken on message rejection is independent of transactions and depends on the `defaultRequeueRejected` property (default: `true`). -For more information about rejecting failed messages, see <>. - -For more information about RabbitMQ transactions and their limitations, see https://www.rabbitmq.com/semantics.html[RabbitMQ Broker Semantics]. - -NOTE: Prior to RabbitMQ 2.7.0, such messages (and any that are unacked when a channel is closed or aborts) went to the back of the queue on a Rabbit broker. -Since 2.7.0, rejected messages go to the front of the queue, in a similar manner to JMS rolled back messages. - -NOTE: Previously, message requeue on transaction rollback was inconsistent between local transactions and when a `TransactionManager` was provided. -In the former case, the normal requeue logic (`AmqpRejectAndDontRequeueException` or `defaultRequeueRejected=false`) applied (see <>). -With a transaction manager, the message was unconditionally requeued on rollback. -Starting with version 2.0, the behavior is consistent and the normal requeue logic is applied in both cases. -To revert to the previous behavior, you can set the container's `alwaysRequeueWithTxManagerRollback` property to `true`. -See <>. - -===== Using `RabbitTransactionManager` - -The https://docs.spring.io/spring-amqp/docs/latest_ga/api/org/springframework/amqp/rabbit/transaction/RabbitTransactionManager.html[RabbitTransactionManager] is an alternative to executing Rabbit operations within, and synchronized with, external transactions. -This transaction manager is an implementation of the https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/transaction/PlatformTransactionManager.html[`PlatformTransactionManager`] interface and should be used with a single Rabbit `ConnectionFactory`. - -IMPORTANT: This strategy is not able to provide XA transactions -- for example, in order to share transactions between messaging and database access. - -Application code is required to retrieve the transactional Rabbit resources through `ConnectionFactoryUtils.getTransactionalResourceHolder(ConnectionFactory, boolean)` instead of a standard `Connection.createChannel()` call with subsequent channel creation. -When using Spring AMQP's https://docs.spring.io/spring-amqp/docs/latest_ga/api/org/springframework/amqp/rabbit/core/RabbitTemplate.html[RabbitTemplate], it will autodetect a thread-bound Channel and automatically participate in its transaction. - -With Java Configuration, you can setup a new RabbitTransactionManager by using the following bean: - -==== -[source,java] ----- -@Bean -public RabbitTransactionManager rabbitTransactionManager() { - return new RabbitTransactionManager(connectionFactory); -} ----- -==== - -If you prefer XML configuration, you can declare the following bean in your XML Application Context file: - -==== -[source,xml] ----- - - - ----- -==== - -[[tx-sync]] -===== Transaction Synchronization - -Synchronizing a RabbitMQ transaction with some other (e.g. DBMS) transaction provides "Best Effort One Phase Commit" semantics. -It is possible that the RabbitMQ transaction fails to commit during the after completion phase of transaction synchronization. -This is logged by the `spring-tx` infrastructure as an error, but no exception is thrown to the calling code. -Starting with version 2.3.10, you can call `ConnectionUtils.checkAfterCompletion()` after the transaction has committed on the same thread that processed the transaction. -It will simply return if no exception occurred; otherwise it will throw an `AfterCompletionFailedException` which will have a property representing the synchronization status of the completion. - -Enable this feature by calling `ConnectionFactoryUtils.enableAfterCompletionFailureCapture(true)`; this is a global flag and applies to all threads. - -[[containerAttributes]] -==== Message Listener Container Configuration - -There are quite a few options for configuring a `SimpleMessageListenerContainer` (SMLC) and a `DirectMessageListenerContainer` (DMLC) related to transactions and quality of service, and some of them interact with each other. -Properties that apply to the SMLC, DMLC, or `StreamListenerContainer` (StLC) (see <>) are indicated by the check mark in the appropriate column. -See <> for information to help you decide which container is appropriate for your application. - -The following table shows the container property names and their equivalent attribute names (in parentheses) when using the namespace to configure a ``. -The `type` attribute on that element can be `simple` (default) or `direct` to specify an `SMLC` or `DMLC` respectively. -Some properties are not exposed by the namespace. -These are indicated by `N/A` for the attribute. - -.Configuration options for a message listener container -[cols="8,16,1,1,1", options="header"] -|=== -|Property -(Attribute) -|Description -|SMLC -|DMLC -|StLC - -|[[ackTimeout]]<> + -(N/A) - -|When `messagesPerAck` is set, this timeout is used as an alternative to send an ack. -When a new message arrives, the count of unacked messages is compared to `messagesPerAck`, and the time since the last ack is compared to this value. -If either condition is `true`, the message is acknowledged. -When no new messages arrive and there are unacked messages, this timeout is approximate since the condition is only checked each `monitorInterval`. -See also `messagesPerAck` and `monitorInterval` in this table. - -a| -a|image::images/tickmark.png[] -a| - -|[[acknowledgeMode]]<> + -(acknowledge) - -a| -* `NONE`: No acks are sent (incompatible with `channelTransacted=true`). -RabbitMQ calls this "`autoack`", because the broker assumes all messages are acked without any action from the consumer. -* `MANUAL`: The listener must acknowledge all messages by calling `Channel.basicAck()`. -* `AUTO`: The container acknowledges the message automatically, unless the `MessageListener` throws an exception. -Note that `acknowledgeMode` is complementary to `channelTransacted` -- if the channel is transacted, the broker requires a commit notification in addition to the ack. -This is the default mode. -See also `batchSize`. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[adviceChain]]<> + -(advice-chain) - -|An array of AOP Advice to apply to the listener execution. -This can be used to apply additional cross-cutting concerns, such as automatic retry in the event of broker death. -Note that simple re-connection after an AMQP error is handled by the `CachingConnectionFactory`, as long as the broker is still alive. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[afterReceivePostProcessors]]<> + -(N/A) - -|An array of `MessagePostProcessor` instances that are invoked before invoking the listener. -Post processors can implement `PriorityOrdered` or `Ordered`. -The array is sorted with un-ordered members invoked last. -If a post processor returns `null`, the message is discarded (and acknowledged, if appropriate). - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[alwaysRequeueWithTxManagerRollback]]<> + -(N/A) - -|Set to `true` to always requeue messages on rollback when a transaction manager is configured. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[autoDeclare]]<> + -(auto-declare) - -a|When set to `true` (default), the container uses a `RabbitAdmin` to redeclare all AMQP objects (queues, exchanges, bindings), if it detects that at least one of its queues is missing during startup, perhaps because it is an `auto-delete` or an expired queue, but the redeclaration proceeds if the queue is missing for any reason. -To disable this behavior, set this property to `false`. -Note that the container fails to start if all of its queues are missing. - -NOTE: Prior to version 1.6, if there was more than one admin in the context, the container would randomly select one. -If there were no admins, it would create one internally. -In either case, this could cause unexpected results. -Starting with version 1.6, for `autoDeclare` to work, there must be exactly one `RabbitAdmin` in the context, or a reference to a specific instance must be configured on the container using the `rabbitAdmin` property. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[autoStartup]]<> + -(auto-startup) - -|Flag to indicate that the container should start when the `ApplicationContext` does (as part of the `SmartLifecycle` callbacks, which happen after all beans are initialized). -Defaults to `true`, but you can set it to `false` if your broker might not be available on startup and call `start()` later manually when you know the broker is ready. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] - -|[[batchSize]]<> + -(transaction-size) -(batch-size) - -|When used with `acknowledgeMode` set to `AUTO`, the container tries to process up to this number of messages before sending an ack (waiting for each one up to the receive timeout setting). -This is also when a transactional channel is committed. -If the `prefetchCount` is less than the `batchSize`, it is increased to match the `batchSize`. - -a|image::images/tickmark.png[] -a| -a| - -|[[batchingStrategy]]<> + -(N/A) - -|The strategy used when debatchng messages. -Default `SimpleDebatchingStrategy`. -See <> and <>. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[channelTransacted]]<> + -(channel-transacted) - -|Boolean flag to signal that all messages should be acknowledged in a transaction (either manually or automatically). - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[concurrency]]<> + -(N/A) - -|`m-n` The range of concurrent consumers for each listener (min, max). -If only `n` is provided, `n` is a fixed number of consumers. -See <>. - -a|image::images/tickmark.png[] -a| -a| - -|[[concurrentConsumers]]<> + -(concurrency) - -|The number of concurrent consumers to initially start for each listener. -See <>. -For the `StLC`, concurrency is controlled via an overloaded `superStream` method; see <>. - -a|image::images/tickmark.png[] -a| -a|image::images/tickmark.png[] - -|[[connectionFactory]]<> + -(connection-factory) - -|A reference to the `ConnectionFactory`. -When configuring by using the XML namespace, the default referenced bean name is `rabbitConnectionFactory`. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[consecutiveActiveTrigger]]<> + -(min-consecutive-active) - -|The minimum number of consecutive messages received by a consumer, without a receive timeout occurring, when considering starting a new consumer. -Also impacted by 'batchSize'. -See <>. -Default: 10. - -a|image::images/tickmark.png[] -a| -a| - -|[[consecutiveIdleTrigger]]<> + -(min-consecutive-idle) - -|The minimum number of receive timeouts a consumer must experience before considering stopping a consumer. -Also impacted by 'batchSize'. -See <>. -Default: 10. - -a|image::images/tickmark.png[] -a| -a| - -|[[consumerBatchEnabled]]<> + -(batch-enabled) - -|If the `MessageListener` supports it, setting this to true enables batching of discrete messages, up to `batchSize`; a partial batch will be delivered if no new messages arrive in `receiveTimeout`. -When this is false, batching is only supported for batches created by a producer; see <>. - -a|image::images/tickmark.png[] -a| -a| - -|[[consumerCustomizer]]<> + -(N/A) - -|A `ConsumerCustomizer` bean used to modify stream consumers created by the container. - -a| -a| -a|image::images/tickmark.png[] - -|[[consumerStartTimeout]]<> + -(N/A) - -|The time in milliseconds to wait for a consumer thread to start. -If this time elapses, an error log is written. -An example of when this might happen is if a configured `taskExecutor` has insufficient threads to support the container `concurrentConsumers`. - -See <>. -Default: 60000 (one minute). - -a|image::images/tickmark.png[] -a| -a| - -|[[consumerTagStrategy]]<> + -(consumer-tag-strategy) - -|Set an implementation of <>, enabling the creation of a (unique) tag for each consumer. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[consumersPerQueue]]<> + -(consumers-per-queue) - -|The number of consumers to create for each configured queue. -See <>. - -a| -a|image::images/tickmark.png[] -a| - -|[[consumeDelay]]<> + -(N/A) - -|When using the https://github.com/rabbitmq/rabbitmq-sharding[RabbitMQ Sharding Plugin] with `concurrentConsumers > 1`, there is a race condition that can prevent even distribution of the consumers across the shards. -Use this property to add a small delay between consumer starts to avoid this race condition. -You should experiment with values to determine the suitable delay for your environment. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[debatchingEnabled]]<> + -(N/A) - -|When true, the listener container will debatch batched messages and invoke the listener with each message from the batch. -Starting with version 2.2.7, <> will be debatched as a `List` if the listener is a `BatchMessageListener` or `ChannelAwareBatchMessageListener`. -Otherwise messages from the batch are presented one-at-a-time. -Default true. -See <> and <>. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[declarationRetries]]<> + -(declaration-retries) - -|The number of retry attempts when passive queue declaration fails. -Passive queue declaration occurs when the consumer starts or, when consuming from multiple queues, when not all queues were available during initialization. -When none of the configured queues can be passively declared (for any reason) after the retries are exhausted, the container behavior is controlled by the 'missingQueuesFatal` property, described earlier. -Default: Three retries (for a total of four attempts). - -a|image::images/tickmark.png[] -a| -a| - -|[[defaultRequeueRejected]]<> + -(requeue-rejected) - -|Determines whether messages that are rejected because the listener threw an exception should be requeued or not. -Default: `true`. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[errorHandler]]<> + -(error-handler) - -|A reference to an `ErrorHandler` strategy for handling any uncaught exceptions that may occur during the execution of the MessageListener. -Default: `ConditionalRejectingErrorHandler` - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[exclusive]]<> + -(exclusive) - -|Determines whether the single consumer in this container has exclusive access to the queues. -The concurrency of the container must be 1 when this is `true`. -If another consumer has exclusive access, the container tries to recover the consumer, according to the -`recovery-interval` or `recovery-back-off`. -When using the namespace, this attribute appears on the `` element along with the queue names. -Default: `false`. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[exclusiveConsumerExceptionLogger]]<> + -(N/A) - -|An exception logger used when an exclusive consumer cannot gain access to a queue. -By default, this is logged at the `WARN` level. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[failedDeclarationRetryInterval]]<> + -(failed-declaration --retry-interval) - -|The interval between passive queue declaration retry attempts. -Passive queue declaration occurs when the consumer starts or, when consuming from multiple queues, when not all queues were available during initialization. -Default: 5000 (five seconds). - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[forceCloseChannel]]<> + -(N/A) - -|If the consumers do not respond to a shutdown within `shutdownTimeout`, if this is `true`, the channel will be closed, causing any unacked messages to be requeued. -Defaults to `true` since 2.0. -You can set it to `false` to revert to the previous behavior. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[forceStop]]<> + -(N/A) - -|Set to true to stop (when the container is stopped) after the current record is processed; causing all prefetched messages to be requeued. -By default, the container will cancel the consumer and process all prefetched messages before stopping. -Since versions 2.4.14, 3.0.6 -Defaults to `false`. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[globalQos]]<> + -(global-qos) - -|When true, the `prefetchCount` is applied globally to the channel rather than to each consumer on the channel. -See https://www.rabbitmq.com/amqp-0-9-1-reference.html#basic.qos.global[`basicQos.global`] for more information. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|(group) - -|This is available only when using the namespace. -When specified, a bean of type `Collection` is registered with this name, and the -container for each `` element is added to the collection. -This allows, for example, starting and stopping the group of containers by iterating over the collection. -If multiple `` elements have the same group value, the containers in the collection form -an aggregate of all containers so designated. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[idleEventInterval]]<> + -(idle-event-interval) - -|See <>. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[javaLangErrorHandler]]<> + -(N/A) - -|An `AbstractMessageListenerContainer.JavaLangErrorHandler` implementation that is called when a container thread catches an `Error`. -The default implementation calls `System.exit(99)`; to revert to the previous behavior (do nothing), add a no-op handler. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[maxConcurrentConsumers]]<> + -(max-concurrency) - -|The maximum number of concurrent consumers to start, if needed, on demand. -Must be greater than or equal to 'concurrentConsumers'. -See <>. - -a|image::images/tickmark.png[] -a| -a| - -|[[messagesPerAck]]<> + -(N/A) - -|The number of messages to receive between acks. -Use this to reduce the number of acks sent to the broker (at the cost of increasing the possibility of redelivered messages). -Generally, you should set this property only on high-volume listener containers. -If this is set and a message is rejected (exception thrown), pending acks are acknowledged and the failed message is rejected. -Not allowed with transacted channels. -If the `prefetchCount` is less than the `messagesPerAck`, it is increased to match the `messagesPerAck`. -Default: ack every message. -See also `ackTimeout` in this table. - -a| -a|image::images/tickmark.png[] -a| - -|[[mismatchedQueuesFatal]]<> + -(mismatched-queues-fatal) - -a|When the container starts, if this property is `true` (default: `false`), the container checks that all queues declared in the context are compatible with queues already on the broker. -If mismatched properties (such as `auto-delete`) or arguments (skuch as `x-message-ttl`) exist, the container (and application context) fails to start with a fatal exception. - -If the problem is detected during recovery (for example, after a lost connection), the container is stopped. - -There must be a single `RabbitAdmin` in the application context (or one specifically configured on the container by using the `rabbitAdmin` property). -Otherwise, this property must be `false`. - -NOTE: If the broker is not available during initial startup, the container starts and the conditions are checked when the connection is established. - -IMPORTANT: The check is done against all queues in the context, not just the queues that a particular listener is configured to use. -If you wish to limit the checks to just those queues used by a container, you should configure a separate `RabbitAdmin` for the container, and provide a reference to it using the `rabbitAdmin` property. -See <> for more information. - -IMPORTANT: Mismatched queue argument detection is disabled while starting a container for a `@RabbitListener` in a bean that is marked `@Lazy`. -This is to avoid a potential deadlock which can delay the start of such containers for up to 60 seconds. -Applications using lazy listener beans should check the queue arguments before getting a reference to the lazy bean. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[missingQueuesFatal]]<> + -(missing-queues-fatal) - -a|When set to `true` (default), if none of the configured queues are available on the broker, it is considered fatal. -This causes the application context to fail to initialize during startup. -Also, when the queues are deleted while the container is running, by default, the consumers make three retries to connect to the queues (at five second intervals) and stop the container if these attempts fail. - -This was not configurable in previous versions. - -When set to `false`, after making the three retries, the container goes into recovery mode, as with other problems, such as the broker being down. -The container tries to recover according to the `recoveryInterval` property. -During each recovery attempt, each consumer again tries four times to passively declare the queues at five second intervals. -This process continues indefinitely. - -You can also use a properties bean to set the property globally for all containers, as follows: - -==== -[source,xml] ----- - - - false - - ----- -==== - -This global property is not applied to any containers that have an explicit `missingQueuesFatal` property set. - -The default retry properties (three retries at five-second intervals) can be overridden by setting the properties below. - -IMPORTANT: Missing queue detection is disabled while starting a container for a `@RabbitListener` in a bean that is marked `@Lazy`. -This is to avoid a potential deadlock which can delay the start of such containers for up to 60 seconds. -Applications using lazy listener beans should check the queue(s) before getting a reference to the lazy bean. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[monitorInterval]]<> + -(monitor-interval) - -|With the DMLC, a task is scheduled to run at this interval to monitor the state of the consumers and recover any that have failed. - -a| -a|image::images/tickmark.png[] -a| - -|[[noLocal]]<> + -(N/A) - -|Set to `true` to disable delivery from the server to consumers messages published on the same channel's connection. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[phase]]<> + -(phase) - -|When `autoStartup` is `true`, the lifecycle phase within which this container should start and stop. -The lower the value, the earlier this container starts and the later it stops. -The default is `Integer.MAX_VALUE`, meaning the container starts as late as possible and stops as soon as possible. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[possibleAuthenticationFailureFatal]]<> + -(possible-authentication-failure-fatal) - -a|When set to `true` (default for SMLC), if a `PossibleAuthenticationFailureException` is thrown during connection, it is considered fatal. -This causes the application context to fail to initialize during startup (if the container is configured with auto startup). - -Since _version 2.0_. - -**DirectMessageListenerContainer** - -When set to `false` (default), each consumer will attempt to reconnect according to the `monitorInterval`. - -**SimpleMessageListenerContainer** - -When set to `false`, after making the 3 retries, the container will go into recovery mode, as with other problems, such as the broker being down. -The container will attempt to recover according to the `recoveryInterval` property. -During each recovery attempt, each consumer will again try 4 times to start. -This process will continue indefinitely. - -You can also use a properties bean to set the property globally for all containers, as follows: - -[source,xml] ----- - - - false - - ----- - -This global property will not be applied to any containers that have an explicit `missingQueuesFatal` property set. - -The default retry properties (3 retries at 5 second intervals) can be overridden using the properties after this one. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[prefetchCount]]<> + -(prefetch) - -a|The number of unacknowledged messages that can be outstanding at each consumer. -The higher this value is, the faster the messages can be delivered, but the higher the risk of non-sequential processing. -Ignored if the `acknowledgeMode` is `NONE`. -This is increased, if necessary, to match the `batchSize` or `messagePerAck`. -Defaults to 250 since 2.0. -You can set it to 1 to revert to the previous behavior. - -IMPORTANT: There are scenarios where the prefetch value should -be low -- for example, with large messages, especially if the processing is slow (messages could add up -to a large amount of memory in the client process), and if strict message ordering is necessary -(the prefetch value should be set back to 1 in this case). -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. - -Also see `globalQos`. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[rabbitAdmin]]<> + -(admin) - -|When a listener container listens to at least one auto-delete queue and it is found to be missing during startup, the container uses a `RabbitAdmin` to declare the queue and any related bindings and exchanges. -If such elements are configured to use conditional declaration (see <>), the container must use the admin that was configured to declare those elements. -Specify that admin here. -It is required only when using auto-delete queues with conditional declaration. -If you do not wish the auto-delete queues to be declared until the container is started, set `auto-startup` to `false` on the admin. -Defaults to a `RabbitAdmin` that declares all non-conditional elements. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[receiveTimeout]]<> + -(receive-timeout) - -|The maximum time to wait for each message. -If `acknowledgeMode=NONE`, this has very little effect -- the container spins round and asks for another message. -It has the biggest effect for a transactional `Channel` with `batchSize > 1`, since it can cause messages already consumed not to be acknowledged until the timeout expires. -When `consumerBatchEnabled` is true, a partial batch will be delivered if this timeout occurs before a batch is complete. - -a|image::images/tickmark.png[] -a| -a| - -|[[recoveryBackOff]]<> + -(recovery-back-off) - -|Specifies the `BackOff` for intervals between attempts to start a consumer if it fails to start for non-fatal reasons. -Default is `FixedBackOff` with unlimited retries every five seconds. -Mutually exclusive with `recoveryInterval`. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[recoveryInterval]]<> + -(recovery-interval) - -|Determines the time in milliseconds between attempts to start a consumer if it fails to start for non-fatal reasons. -Default: 5000. -Mutually exclusive with `recoveryBackOff`. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[retryDeclarationInterval]]<> + -(missing-queue- -retry-interval) - -|If a subset of the configured queues are available during consumer initialization, the consumer starts consuming from those queues. -The consumer tries to passively declare the missing queues by using this interval. -When this interval elapses, the 'declarationRetries' and 'failedDeclarationRetryInterval' is used again. -If there are still missing queues, the consumer again waits for this interval before trying again. -This process continues indefinitely until all queues are available. -Default: 60000 (one minute). - -a|image::images/tickmark.png[] -a| -a| - -|[[shutdownTimeout]]<> + -(N/A) - -|When a container shuts down (for example, -if its enclosing `ApplicationContext` is closed), it waits for in-flight messages to be processed up to this limit. -Defaults to five seconds. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[startConsumerMinInterval]]<> + -(min-start-interval) - -|The time in milliseconds that must elapse before each new consumer is started on demand. -See <>. -Default: 10000 (10 seconds). - -a|image::images/tickmark.png[] -a| -a| - -|[[statefulRetryFatal]]<> + -WithNullMessageId -(N/A) - -|When using a stateful retry advice, if a message with a missing `messageId` property is received, it is considered -fatal for the consumer (it is stopped) by default. -Set this to `false` to discard (or route to a dead-letter queue) such messages. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[stopConsumerMinInterval]]<> + -(min-stop-interval) - -|The time in milliseconds that must elapse before a consumer is stopped since the last consumer was stopped when an idle consumer is detected. -See <>. -Default: 60000 (one minute). - -a|image::images/tickmark.png[] -a| -a| - -|[[streamConverter]]<> + -(N/A) - -|A `StreamMessageConverter` to convert a native Stream message to a Spring AMQP message. - -a| -a| -a|image::images/tickmark.png[] - -|[[taskExecutor]]<> + -(task-executor) - -|A reference to a Spring `TaskExecutor` (or standard JDK 1.5+ `Executor`) for executing listener invokers. -Default is a `SimpleAsyncTaskExecutor`, using internally managed threads. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| - -|[[taskScheduler]]<> + -(task-scheduler) - -|With the DMLC, the scheduler used to run the monitor task at the 'monitorInterval'. - -a| -a|image::images/tickmark.png[] -a| - -|[[transactionManager]]<> + -(transaction-manager) - -|External transaction manager for the operation of the listener. -Also complementary to `channelTransacted` -- if the `Channel` is transacted, its transaction is synchronized with the external transaction. - -a|image::images/tickmark.png[] -a|image::images/tickmark.png[] -a| -|=== - -[[listener-concurrency]] -==== Listener Concurrency - -===== SimpleMessageListenerContainer - -By default, the listener container starts a single consumer that receives messages from the queues. - -When examining the table in the previous section, you can see a number of properties and attributes that control concurrency. -The simplest is `concurrentConsumers`, which creates that (fixed) number of consumers that concurrently process messages. - -Prior to version 1.3.0, this was the only setting available and the container had to be stopped and started again to change the setting. - -Since version 1.3.0, you can now dynamically adjust the `concurrentConsumers` property. -If it is changed while the container is running, consumers are added or removed as necessary to adjust to the new setting. - -In addition, a new property called `maxConcurrentConsumers` has been added and the container dynamically adjusts the concurrency based on workload. -This works in conjunction with four additional properties: `consecutiveActiveTrigger`, `startConsumerMinInterval`, `consecutiveIdleTrigger`, and `stopConsumerMinInterval`. -With the default settings, the algorithm to increase consumers works as follows: - -If the `maxConcurrentConsumers` has not been reached and an existing consumer is active for ten consecutive cycles AND at least 10 seconds has elapsed since the last consumer was started, a new consumer is started. -A consumer is considered active if it received at least one message in `batchSize` * `receiveTimeout` milliseconds. - -With the default settings, the algorithm to decrease consumers works as follows: - -If there are more than `concurrentConsumers` running and a consumer detects ten consecutive timeouts (idle) AND the last consumer was stopped at least 60 seconds ago, a consumer is stopped. -The timeout depends on the `receiveTimeout` and the `batchSize` properties. -A consumer is considered idle if it receives no messages in `batchSize` * `receiveTimeout` milliseconds. -So, with the default timeout (one second) and a `batchSize` of four, stopping a consumer is considered after 40 seconds of idle time (four timeouts correspond to one idle detection). - -NOTE: Practically, consumers can be stopped only if the whole container is idle for some time. -This is because the broker shares its work across all the active consumers. - -Each consumer uses a single channel, regardless of the number of configured queues. - -Starting with version 2.0, the `concurrentConsumers` and `maxConcurrentConsumers` properties can be set with the `concurrency` property -- for example, `2-4`. - -===== Using `DirectMessageListenerContainer` - -With this container, concurrency is based on the configured queues and `consumersPerQueue`. -Each consumer for each queue uses a separate channel, and the concurrency is controlled by the rabbit client library. -By default, at the time of writing, it uses a pool of `DEFAULT_NUM_THREADS = Runtime.getRuntime().availableProcessors() * 2` threads. - -You can configure a `taskExecutor` to provide the required maximum concurrency. - -[[exclusive-consumer]] -==== Exclusive Consumer - -Starting with version 1.3, you can configure the listener container with a single exclusive consumer. -This prevents other containers from consuming from the queues until the current consumer is cancelled. -The concurrency of such a container must be `1`. - -When using exclusive consumers, other containers try to consume from the queues according to the `recoveryInterval` property and log a `WARN` message if the attempt fails. - -[[listener-queues]] -==== Listener Container Queues - -Version 1.3 introduced a number of improvements for handling multiple queues in a listener container. - -Container can be initially configured to listen on zero queues. -Queues can be added and removed at runtime. -The `SimpleMessageListenerContainer` recycles (cancels and re-creates) all consumers when any pre-fetched messages have been processed. -The `DirectMessageListenerContainer` creates/cancels individual consumer(s) for each queue without affecting consumers on other queues. -See the https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.html[Javadoc] for the `addQueues`, `addQueueNames`, `removeQueues` and `removeQueueNames` methods. - -If not all queues are available, the container tries to passively declare (and consume from) the missing queues every 60 seconds. - -Also, if a consumer receives a cancel from the broker (for example, if a queue is deleted) the consumer tries to recover, and the recovered consumer continues to process messages from any other configured queues. -Previously, a cancel on one queue cancelled the entire consumer and, eventually, the container would stop due to the missing queue. - -If you wish to permanently remove a queue, you should update the container before or after deleting to queue, to avoid future attempts trying to consume from it. - -==== Resilience: Recovering from Errors and Broker Failures - -Some of the key (and most popular) high-level features that Spring AMQP provides are to do with recovery and automatic re-connection in the event of a protocol error or broker failure. -We have seen all the relevant components already in this guide, but it should help to bring them all together here and call out the features and recovery scenarios individually. - -The primary reconnection features are enabled by the `CachingConnectionFactory` itself. -It is also often beneficial to use the `RabbitAdmin` auto-declaration features. -In addition, if you care about guaranteed delivery, you probably also need to use the `channelTransacted` flag in `RabbitTemplate` and `SimpleMessageListenerContainer` and the `AcknowledgeMode.AUTO` (or manual if you do the acks yourself) in the `SimpleMessageListenerContainer`. - -[[automatic-declaration]] -===== Automatic Declaration of Exchanges, Queues, and Bindings - -The `RabbitAdmin` component can declare exchanges, queues, and bindings on startup. -It does this lazily, through a `ConnectionListener`. -Consequently, if the broker is not present on startup, it does not matter. -The first time a `Connection` is used (for example, -by sending a message) the listener fires and the admin features is applied. -A further benefit of doing the auto declarations in a listener is that, if the connection is dropped for any reason (for example, -broker death, network glitch, and others), they are applied again when the connection is re-established. - -NOTE: Queues declared this way must have fixed names -- either explicitly declared or generated by the framework for `AnonymousQueue` instances. -Anonymous queues are non-durable, exclusive, and auto-deleting. - -IMPORTANT: Automatic declaration is performed only when the `CachingConnectionFactory` cache mode is `CHANNEL` (the default). -This limitation exists because exclusive and auto-delete queues are bound to the connection. - -Starting with version 2.2.2, the `RabbitAdmin` will detect beans of type `DeclarableCustomizer` and apply the function before actually processing the declaration. -This is useful, for example, to set a new argument (property) before it has first class support within the framework. - -==== -[source, java] ----- -@Bean -public DeclarableCustomizer customizer() { - return dec -> { - if (dec instanceof Queue && ((Queue) dec).getName().equals("my.queue")) { - dec.addArgument("some.new.queue.argument", true); - } - return dec; - }; -} ----- -==== - -It is also useful in projects that don't provide direct access to the `Declarable` bean definitions. - -See also <>. - -[[retry]] -===== Failures in Synchronous Operations and Options for Retry - -If you lose your connection to the broker in a synchronous sequence when using `RabbitTemplate` (for instance), Spring AMQP throws an `AmqpException` (usually, but not always, `AmqpIOException`). -We do not try to hide the fact that there was a problem, so you have to be able to catch and respond to the exception. -The easiest thing to do if you suspect that the connection was lost (and it was not your fault) is to try the operation again. -You can do this manually, or you could look at using Spring Retry to handle the retry (imperatively or declaratively). - -Spring Retry provides a couple of AOP interceptors and a great deal of flexibility to specify the parameters of the retry (number of attempts, exception types, backoff algorithm, and others). -Spring AMQP also provides some convenience factory beans for creating Spring Retry interceptors in a convenient form for AMQP use cases, with strongly typed callback interfaces that you can use to implement custom recovery logic. -See the Javadoc and properties of `StatefulRetryOperationsInterceptor` and `StatelessRetryOperationsInterceptor` for more detail. -Stateless retry is appropriate if there is no transaction or if a transaction is started inside the retry callback. -Note that stateless retry is simpler to configure and analyze than stateful retry, but it is not usually appropriate if there is an ongoing transaction that must be rolled back or definitely is going to roll back. -A dropped connection in the middle of a transaction should have the same effect as a rollback. -Consequently, for reconnections where the transaction is started higher up the stack, stateful retry is usually the best choice. -Stateful retry needs a mechanism to uniquely identify a message. -The simplest approach is to have the sender put a unique value in the `MessageId` message property. -The provided message converters provide an option to do this: you can set `createMessageIds` to `true`. -Otherwise, you can inject a `MessageKeyGenerator` implementation into the interceptor. -The key generator must return a unique key for each message. -In versions prior to version 2.0, a `MissingMessageIdAdvice` was provided. -It enabled messages without a `messageId` property to be retried exactly once (ignoring the retry settings). -This advice is no longer provided, since, along with `spring-retry` version 1.2, its functionality is built into the interceptor and message listener containers. - -NOTE: For backwards compatibility, a message with a null message ID is considered fatal for the consumer (consumer is stopped) by default (after one retry). -To replicate the functionality provided by the `MissingMessageIdAdvice`, you can set the `statefulRetryFatalWithNullMessageId` property to `false` on the listener container. -With that setting, the consumer continues to run and the message is rejected (after one retry). -It is discarded or routed to the dead letter queue (if one is configured). - -Starting with version 1.3, a builder API is provided to aid in assembling these interceptors by using Java (in `@Configuration` classes). -The following example shows how to do so: - -==== -[source,java] ----- -@Bean -public StatefulRetryOperationsInterceptor interceptor() { - return RetryInterceptorBuilder.stateful() - .maxAttempts(5) - .backOffOptions(1000, 2.0, 10000) // initialInterval, multiplier, maxInterval - .build(); -} ----- -==== - -Only a subset of retry capabilities can be configured this way. -More advanced features would need the configuration of a `RetryTemplate` as a Spring bean. -See the https://docs.spring.io/spring-retry/docs/api/current/[Spring Retry Javadoc] for complete information about available policies and their configuration. - -[[batch-retry]] -===== Retry with Batch Listeners - -It is not recommended to configure retry with a batch listener, unless the batch was created by the producer, in a single record. -See <> for information about consumer and producer-created batches. -With a consumer-created batch, the framework has no knowledge about which message in the batch caused the failure so recovery after the retries are exhausted is not possible. -With producer-created batches, since there is only one message that actually failed, the whole message can be recovered. -Applications may want to inform a custom recoverer where in the batch the failure occurred, perhaps by setting an index property of the thrown exception. - -A retry recoverer for a batch listener must implement `MessageBatchRecoverer`. - -[[async-listeners]] -===== Message Listeners and the Asynchronous Case - -If a `MessageListener` fails because of a business exception, the exception is handled by the message listener container, which then goes back to listening for another message. -If the failure is caused by a dropped connection (not a business exception), the consumer that is collecting messages for the listener has to be cancelled and restarted. -The `SimpleMessageListenerContainer` handles this seamlessly, and it leaves a log to say that the listener is being restarted. -In fact, it loops endlessly, trying to restart the consumer. -Only if the consumer is very badly behaved indeed will it give up. -One side effect is that if the broker is down when the container starts, it keeps trying until a connection can be established. - -Business exception handling, as opposed to protocol errors and dropped connections, might need more thought and some custom configuration, especially if transactions or container acks are in use. -Prior to 2.8.x, RabbitMQ had no definition of dead letter behavior. -Consequently, by default, a message that is rejected or rolled back because of a business exception can be redelivered endlessly. -To put a limit on the client on the number of re-deliveries, one choice is a `StatefulRetryOperationsInterceptor` in the advice chain of the listener. -The interceptor can have a recovery callback that implements a custom dead letter action -- whatever is appropriate for your particular environment. - -Another alternative is to set the container's `defaultRequeueRejected` property to `false`. -This causes all failed messages to be discarded. -When using RabbitMQ 2.8.x or higher, this also facilitates delivering the message to a dead letter exchange. - -Alternatively, you can throw a `AmqpRejectAndDontRequeueException`. -Doing so prevents message requeuing, regardless of the setting of the `defaultRequeueRejected` property. - -Starting with version 2.1, an `ImmediateRequeueAmqpException` is introduced to perform exactly the opposite logic: the message will be requeued, regardless of the setting of the `defaultRequeueRejected` property. - -Often, a combination of both techniques is used. -You can use a `StatefulRetryOperationsInterceptor` in the advice chain with a `MessageRecoverer` that throws an `AmqpRejectAndDontRequeueException`. -The `MessageRecover` is called when all retries have been exhausted. -The `RejectAndDontRequeueRecoverer` does exactly that. -The default `MessageRecoverer` consumes the errant message and emits a `WARN` message. - -Starting with version 1.3, a new `RepublishMessageRecoverer` is provided, to allow publishing of failed messages after retries are exhausted. - -When a recoverer consumes the final exception, the message is ack'd and is not sent to the dead letter exchange by the broker, if configured. - -NOTE: When `RepublishMessageRecoverer` is used on the consumer side, the received message has `deliveryMode` in the `receivedDeliveryMode` message property. -In this case the `deliveryMode` is `null`. -That means a `NON_PERSISTENT` delivery mode on the broker. -Starting with version 2.0, you can configure the `RepublishMessageRecoverer` for the `deliveryMode` to set into the message to republish if it is `null`. -By default, it uses `MessageProperties` default value - `MessageDeliveryMode.PERSISTENT`. - -The following example shows how to set a `RepublishMessageRecoverer` as the recoverer: - -==== -[source,java] ----- -@Bean -RetryOperationsInterceptor interceptor() { - return RetryInterceptorBuilder.stateless() - .maxAttempts(5) - .recoverer(new RepublishMessageRecoverer(amqpTemplate(), "something", "somethingelse")) - .build(); -} ----- -==== - -The `RepublishMessageRecoverer` publishes the message with additional information in message headers, such as the exception message, stack trace, original exchange, and routing key. -Additional headers can be added by creating a subclass and overriding `additionalHeaders()`. -The `deliveryMode` (or any other properties) can also be changed in the `additionalHeaders()`, as the following example shows: - -==== -[source,java] ----- -RepublishMessageRecoverer recoverer = new RepublishMessageRecoverer(amqpTemplate, "error") { - - protected Map additionalHeaders(Message message, Throwable cause) { - message.getMessageProperties() - .setDeliveryMode(message.getMessageProperties().getReceivedDeliveryMode()); - return null; - } - -}; ----- -==== - -Starting with version 2.0.5, the stack trace may be truncated if it is too large; this is because all headers have to fit in a single frame. -By default, if the stack trace would cause less than 20,000 bytes ('headroom') to be available for other headers, it will be truncated. -This can be adjusted by setting the recoverer's `frameMaxHeadroom` property, if you need more or less space for other headers. -Starting with versions 2.1.13, 2.2.3, the exception message is included in this calculation, and the amount of stack trace will be maximized using the following algorithm: - -* if the stack trace alone would exceed the limit, the exception message header will be truncated to 97 bytes plus `...` and the stack trace is truncated too. -* if the stack trace is small, the message will be truncated (plus `...`) to fit in the available bytes (but the message within the stack trace itself is truncated to 97 bytes plus `...`). - -Whenever a truncation of any kind occurs, the original exception will be logged to retain the complete information. -The evaluation is performed after the headers are enhanced so information such as the exception type can be used in the expressions. - -Starting with version 2.4.8, the error exchange and routing key can be provided as SpEL expressions, with the `Message` being the root object for the evaluation. - -Starting with version 2.3.3, a new subclass `RepublishMessageRecovererWithConfirms` is provided; this supports both styles of publisher confirms and will wait for the confirmation before returning (or throw an exception if not confirmed or the message is returned). - -If the confirm type is `CORRELATED`, the subclass will also detect if a message is returned and throw an `AmqpMessageReturnedException`; if the publication is negatively acknowledged, it will throw an `AmqpNackReceivedException`. - -If the confirm type is `SIMPLE`, the subclass will invoke the `waitForConfirmsOrDie` method on the channel. - -See <> for more information about confirms and returns. - -Starting with version 2.1, an `ImmediateRequeueMessageRecoverer` is added to throw an `ImmediateRequeueAmqpException`, which notifies a listener container to requeue the current failed message. - -===== Exception Classification for Spring Retry - -Spring Retry has a great deal of flexibility for determining which exceptions can invoke retry. -The default configuration retries for all exceptions. -Given that user exceptions are wrapped in a `ListenerExecutionFailedException`, we need to ensure that the classification examines the exception causes. -The default classifier looks only at the top level exception. - -Since Spring Retry 1.0.3, the `BinaryExceptionClassifier` has a property called `traverseCauses` (default: `false`). -When `true`, it travers exception causes until it finds a match or there is no cause. - -To use this classifier for retry, you can use a `SimpleRetryPolicy` created with the constructor that takes the max attempts, the `Map` of `Exception` instances, and the boolean (`traverseCauses`) and inject this policy into the `RetryTemplate`. - -[[multi-rabbit]] -==== Multiple Broker (or Cluster) Support - -Version 2.3 added more convenience when communicating between a single application and multiple brokers or broker clusters. -The main benefit, on the consumer side, is that the infrastructure can automatically associate auto-declared queues with the appropriate broker. - -This is best illustrated with an example: - -==== -[source, java] ----- -@SpringBootApplication(exclude = RabbitAutoConfiguration.class) -public class Application { - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } - - @Bean - CachingConnectionFactory cf1() { - return new CachingConnectionFactory("localhost"); - } - - @Bean - CachingConnectionFactory cf2() { - return new CachingConnectionFactory("otherHost"); - } - - @Bean - CachingConnectionFactory cf3() { - return new CachingConnectionFactory("thirdHost"); - } - - @Bean - SimpleRoutingConnectionFactory rcf(CachingConnectionFactory cf1, - CachingConnectionFactory cf2, CachingConnectionFactory cf3) { - - SimpleRoutingConnectionFactory rcf = new SimpleRoutingConnectionFactory(); - rcf.setDefaultTargetConnectionFactory(cf1); - rcf.setTargetConnectionFactories(Map.of("one", cf1, "two", cf2, "three", cf3)); - return rcf; - } - - @Bean("factory1-admin") - RabbitAdmin admin1(CachingConnectionFactory cf1) { - return new RabbitAdmin(cf1); - } - - @Bean("factory2-admin") - RabbitAdmin admin2(CachingConnectionFactory cf2) { - return new RabbitAdmin(cf2); - } - - @Bean("factory3-admin") - RabbitAdmin admin3(CachingConnectionFactory cf3) { - return new RabbitAdmin(cf3); - } - - @Bean - public RabbitListenerEndpointRegistry rabbitListenerEndpointRegistry() { - return new RabbitListenerEndpointRegistry(); - } - - @Bean - public RabbitListenerAnnotationBeanPostProcessor postProcessor(RabbitListenerEndpointRegistry registry) { - MultiRabbitListenerAnnotationBeanPostProcessor postProcessor - = new MultiRabbitListenerAnnotationBeanPostProcessor(); - postProcessor.setEndpointRegistry(registry); - postProcessor.setContainerFactoryBeanName("defaultContainerFactory"); - return postProcessor; - } - - @Bean - public SimpleRabbitListenerContainerFactory factory1(CachingConnectionFactory cf1) { - SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); - factory.setConnectionFactory(cf1); - return factory; - } - - @Bean - public SimpleRabbitListenerContainerFactory factory2(CachingConnectionFactory cf2) { - SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); - factory.setConnectionFactory(cf2); - return factory; - } - - @Bean - public SimpleRabbitListenerContainerFactory factory3(CachingConnectionFactory cf3) { - SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); - factory.setConnectionFactory(cf3); - return factory; - } - - @Bean - RabbitTemplate template(SimpleRoutingConnectionFactory rcf) { - return new RabbitTemplate(rcf); - } - - @Bean - ConnectionFactoryContextWrapper wrapper(SimpleRoutingConnectionFactory rcf) { - return new ConnectionFactoryContextWrapper(rcf); - } - -} - -@Component -class Listeners { - - @RabbitListener(queuesToDeclare = @Queue("q1"), containerFactory = "factory1") - public void listen1(String in) { - - } - - @RabbitListener(queuesToDeclare = @Queue("q2"), containerFactory = "factory2") - public void listen2(String in) { - - } - - @RabbitListener(queuesToDeclare = @Queue("q3"), containerFactory = "factory3") - public void listen3(String in) { - - } - -} ----- -==== - -As you can see, we have declared 3 sets of infrastructure (connection factories, admins, container factories). -As discussed earlier, `@RabbitListener` can define which container factory to use; in this case, they also use `queuesToDeclare` which causes the queue(s) to be declared on the broker, if it doesn't exist. -By naming the `RabbitAdmin` beans with the convention `-admin`, the infrastructure is able to determine which admin should declare the queue. -This will also work with `bindings = @QueueBinding(...)` whereby the exchange and binding will also be declared. -It will NOT work with `queues`, since that expects the queue(s) to already exist. - -On the producer side, a convenient `ConnectionFactoryContextWrapper` class is provided, to make using the `RoutingConnectionFactory` (see <>) simpler. - -As you can see above, a `SimpleRoutingConnectionFactory` bean has been added with routing keys `one`, `two` and `three`. -There is also a `RabbitTemplate` that uses that factory. -Here is an example of using that template with the wrapper to route to one of the broker clusters. - -==== -[source, java] ----- -@Bean -public ApplicationRunner runner(RabbitTemplate template, ConnectionFactoryContextWrapper wrapper) { - return args -> { - wrapper.run("one", () -> template.convertAndSend("q1", "toCluster1")); - wrapper.run("two", () -> template.convertAndSend("q2", "toCluster2")); - wrapper.run("three", () -> template.convertAndSend("q3", "toCluster3")); - }; -} ----- -==== - -==== Debugging - -Spring AMQP provides extensive logging, especially at the `DEBUG` level. - -If you wish to monitor the AMQP protocol between the application and broker, you can use a tool such as WireShark, which has a plugin to decode the protocol. -Alternatively, the RabbitMQ Java client comes with a very useful class called `Tracer`. -When run as a `main`, by default, it listens on port 5673 and connects to port 5672 on localhost. -You can run it and change your connection factory configuration to connect to port 5673 on localhost. -It displays the decoded protocol on the console. -Refer to the `Tracer` Javadoc for more information. diff --git a/src/reference/asciidoc/appendix.adoc b/src/reference/asciidoc/appendix.adoc deleted file mode 100644 index 60bee0b65a..0000000000 --- a/src/reference/asciidoc/appendix.adoc +++ /dev/null @@ -1,1329 +0,0 @@ -[appendix] -[[observation-gen]] -== Micrometer Observation Documentation - -include::../docs/generated/metrics.adoc[] - -include::../docs/generated/spans.adoc[] - -include::../docs/generated/conventions.adoc[] - -[appendix] -[[native-images]] -== Native Images - -https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aot[Spring AOT] native hints are provided to assist in developing native images for Spring applications that use Spring AMQP. - -Some examples can be seen in the https://github.com/spring-projects/spring-aot-smoke-tests/tree/main/integration[`spring-aot-smoke-tests` GitHub repository]. - -[appendix] -[[change-history]] -== Change History - -This section describes changes that have been made as versions have changed. - -=== Current Release - -See <>. - -[[previous-whats-new]] -=== Previous Releases - -==== Changes in 3.0 Since 2.4 - -===== Java 17, Spring Framework 6.0 - -This version requires Spring Framework 6.0 and Java 17 - -===== Remoting - -The remoting feature (using RMI) is no longer supported. - -===== Observation - -Enabling observation for timers and tracing using Micrometer is now supported. -See <> for more information. - -[[x30-Native]] -===== Native Images - -Support for creating native images is provided. -See <> for more information. - -===== AsyncRabbitTemplate - -IMPORTANT: The `AsyncRabbitTemplate` now returns `CompletableFuture` s instead of `ListenableFuture` s. -See <> for more information. - -===== Stream Support Changes - -IMPORTANT: `RabbitStreamOperations` and `RabbitStreamTemplate` methods now return `CompletableFuture` instead of `ListenableFuture`. - -Super streams and single active consumers thereon are now supported. - -See <> 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 <> for more information. - -`MessageConverter` s can now return `Optional.empty()` for a null value; this is currently implemented by the `Jackson2JsonMessageConverter`. -See <> for more information - -You can now configure a `ReplyPostProcessor` via the container factory rather than via a property on `@RabbitListener`. -See <> for more information. - -The `@RabbitListener` (and `@RabbitHandler`) methods can now be declared as Kotlin `suspend` functions. -See <> for more information. - -Starting with version 3.0.5, listeners with async return types (including Kotlin suspend functions) invoke the `RabbitListenerErrorHandler` (if configured) after a failure. -Previously, the error handler was only invoked with synchronous invocations. - -===== Connection Factory Changes - -The default `addressShuffleMode` in `AbstractConnectionFactory` is now `RANDOM`. -This results in connecting to a random host when multiple addresses are provided. -See <> for more information. - -The `LocalizedQueueConnectionFactory` no longer uses the RabbitMQ `http-client` library to determine which node is the leader for a queue. -See <> for more information. - -==== Changes in 2.4 Since 2.3 - -This section describes the changes between version 2.3 and version 2.4. -See <> for changes in previous versions. - -===== `@RabbitListener` Changes - -`MessageProperties` is now available for argument matching. -See <> for more information. - -===== `RabbitAdmin` Changes - -A new property `recoverManualDeclarations` allows recovery of manually declared queues/exchanges/bindings. -See <> for more information. - -===== Remoting Support - -Support remoting using Spring Framework’s RMI support is deprecated and will be removed in 3.0. See Spring Remoting with AMQP for more information. - -==== Message Converter Changes - -The `Jackson2JsonMessageConverter` can now determine the charset from the `contentEncoding` header. -See <> for more information. - -==== Message Converter Changes - -The `Jackson2JsonMessageConverter` can now determine the charset from the `contentEncoding` header. -See <> for more information. - -==== Stream Support Changes - -`RabbitStreamOperations` and `RabbitStreamTemplate` have been deprecated in favor of `RabbitStreamOperations2` and `RabbitStreamTemplate2` respectively; they return `CompletableFuture` instead of `ListenableFuture`. -See <> for more information. - -==== Changes in 2.3 Since 2.2 - -This section describes the changes between version 2.2 and version 2.3. -See <> for changes in previous versions. - -===== Connection Factory Changes - -Two additional connection factories are now provided. -See <> for more information. - -===== `@RabbitListener` Changes - -You can now specify a reply content type. -See <> for more information. - -===== Message Converter Changes - -The `Jackson2JMessageConverter` s can now deserialize abstract classes (including interfaces) if the `ObjectMapper` is configured with a custom deserializer. -See <> for more information. - -===== Testing Changes - -A new annotation `@SpringRabbitTest` is provided to automatically configure some infrastructure beans for when you are not using `SpringBootTest`. -See <> for more information. - -===== RabbitTemplate Changes - -The template's `ReturnCallback` has been refactored as `ReturnsCallback` for simpler use in lambda expressions. -See <> for more information. - -When using returns and correlated confirms, the `CorrelationData` now requires a unique `id` property. -See <> for more information. - -When using direct reply-to, you can now configure the template such that the server does not need to return correlation data with the reply. -See <> for more information. - -===== Listener Container Changes - -A new listener container property `consumeDelay` is now available; it is helpful when using the https://github.com/rabbitmq/rabbitmq-sharding[RabbitMQ Sharding Plugin]. - -The default `JavaLangErrorHandler` now calls `System.exit(99)`. -To revert to the previous behavior (do nothing), add a no-op handler. - -The containers now support the `globalQos` property to apply the `prefetchCount` globally for the channel rather than for each consumer on the channel. - -See <> for more information. - -===== MessagePostProcessor Changes - -The compressing `MessagePostProcessor` s now use a comma to separate multiple content encodings instead of a colon. -The decompressors can handle both formats but, if you produce messages with this version that are consumed by versions earlier than 2.2.12, you should configure the compressor to use the old delimiter. -See the IMPORTANT note in <> for more information. - -===== Multiple Broker Support Improvements - -See <> for more information. - -===== RepublishMessageRecoverer Changes - -A new subclass of this recoverer is not provided that supports publisher confirms. -See <> for more information. - -==== Changes in 2.2 Since 2.1 - -This section describes the changes between version 2.1 and version 2.2. - -===== Package Changes - -The following classes/interfaces have been moved from `org.springframework.amqp.rabbit.core.support` to `org.springframework.amqp.rabbit.batch`: - -* `BatchingStrategy` -* `MessageBatch` -* `SimpleBatchingStrategy` - -In addition, `ListenerExecutionFailedException` has been moved from `org.springframework.amqp.rabbit.listener.exception` to `org.springframework.amqp.rabbit.support`. - -===== Dependency Changes - -JUnit (4) is now an optional dependency and will no longer appear as a transitive dependency. - -The `spring-rabbit-junit` module is now a *compile* dependency in the `spring-rabbit-test` module for a better target application development experience when with only a single `spring-rabbit-test` we get the full stack of testing utilities for AMQP components. - -===== "Breaking" API Changes - -the JUnit (5) `RabbitAvailableCondition.getBrokerRunning()` now returns a `BrokerRunningSupport` instance instead of a `BrokerRunning`, which depends on JUnit 4. -It has the same API so it's just a matter of changing the class name of any references. -See <> for more information. - -===== ListenerContainer Changes - -Messages with fatal exceptions are now rejected and NOT requeued, by default, even if the acknowledge mode is manual. -See <> for more information. - -Listener performance can now be monitored using Micrometer `Timer` s. -See <> for more information. - -===== @RabbitListener Changes - -You can now configure an `executor` on each listener, overriding the factory configuration, to more easily identify threads associated with the listener. -You can now override the container factory's `acknowledgeMode` property with the annotation's `ackMode` property. -See <> for more information. - -When using <>, `@RabbitListener` methods can now receive a complete batch of messages in one call instead of getting them one-at-a-time. - -When receiving batched messages one-at-a-time, the last message has the `isLastInBatch` message property set to true. - -In addition, received batched messages now contain the `amqp_batchSize` header. - -Listeners can also consume batches created in the `SimpleMessageListenerContainer`, even if the batch is not created by the producer. -See <> for more information. - -Spring Data Projection interfaces are now supported by the `Jackson2JsonMessageConverter`. -See <> for more information. - -The `Jackson2JsonMessageConverter` now assumes the content is JSON if there is no `contentType` property, or it is the default (`application/octet-string`). -See <> for more information. - -Similarly. the `Jackson2XmlMessageConverter` now assumes the content is XML if there is no `contentType` property, or it is the default (`application/octet-string`). -See <> for more information. - -When a `@RabbitListener` method returns a result, the bean and `Method` are now available in the reply message properties. -This allows configuration of a `beforeSendReplyMessagePostProcessor` to, for example, set a header in the reply to indicate which method was invoked on the server. -See <> for more information. - -You can now configure a `ReplyPostProcessor` to make modifications to a reply message before it is sent. -See <> for more information. - -===== AMQP Logging Appenders Changes - -The Log4J and Logback `AmqpAppender` s now support a `verifyHostname` SSL option. - -Also these appenders now can be configured to not add MDC entries as headers. -The `addMdcAsHeaders` boolean option has been introduces to configure such a behavior. - -The appenders now support the `SaslConfig` property. - -See <> for more information. - -===== MessageListenerAdapter Changes - -The `MessageListenerAdapter` provides now a new `buildListenerArguments(Object, Channel, Message)` method to build an array of arguments to be passed into target listener and an old one is deprecated. -See <> for more information. - -===== Exchange/Queue Declaration Changes - -The `ExchangeBuilder` and `QueueBuilder` fluent APIs used to create `Exchange` and `Queue` objects for declaration by `RabbitAdmin` now support "well known" arguments. -See <> for more information. - -The `RabbitAdmin` has a new property `explicitDeclarationsOnly`. -See <> for more information. - -===== Connection Factory Changes - -The `CachingConnectionFactory` has a new property `shuffleAddresses`. -When providing a list of broker node addresses, the list will be shuffled before creating a connection so that the order in which the connections are attempted is random. -See <> for more information. - -When using Publisher confirms and returns, the callbacks are now invoked on the connection factory's `executor`. -This avoids a possible deadlock in the `amqp-clients` library if you perform rabbit operations from within the callback. -See <> for more information. - -Also, the publisher confirm type is now specified with the `ConfirmType` enum instead of the two mutually exclusive setter methods. - -The `RabbitConnectionFactoryBean` now uses TLS 1.2 by default when SSL is enabled. -See <> for more information. - -===== New MessagePostProcessor Classes - -Classes `DeflaterPostProcessor` and `InflaterPostProcessor` were added to support compression and decompression, respectively, when the message content-encoding is set to `deflate`. - -===== Other Changes - -The `Declarables` object (for declaring multiple queues, exchanges, bindings) now has a filtered getter for each type. -See <> for more information. - -You can now customize each `Declarable` bean before the `RabbitAdmin` processes the declaration thereof. -See <> for more information. - -`singleActiveConsumer()` has been added to the `QueueBuilder` to set the `x-single-active-consumer` queue argument. -See <> for more information. - -Outbound headers with values of type `Class` are now mapped using `getName()` instead of `toString()`. -See <> for more information. - -Recovery of failed producer-created batches is now supported. -See <> for more information. - -==== Changes in 2.1 Since 2.0 - -===== AMQP Client library - -Spring AMQP now uses the 5.4.x version of the `amqp-client` library provided by the RabbitMQ team. -This client has auto-recovery configured by default. -See <>. - -NOTE: As of version 4.0, the client enables automatic recovery by default. -While compatible with this feature, Spring AMQP has its own recovery mechanisms and the client recovery feature generally is not needed. -We recommend disabling `amqp-client` automatic recovery, to avoid getting `AutoRecoverConnectionNotCurrentlyOpenException` instances when the broker is available but the connection has not yet recovered. -Starting with version 1.7.1, Spring AMQP disables it unless you explicitly create your own RabbitMQ connection factory and provide it to the `CachingConnectionFactory`. -RabbitMQ `ConnectionFactory` instances created by the `RabbitConnectionFactoryBean` also have the option disabled by default. - - -===== Package Changes - -Certain classes have moved to different packages. -Most are internal classes and do not affect user applications. -Two exceptions are `ChannelAwareMessageListener` and `RabbitListenerErrorHandler`. -These interfaces are now in `org.springframework.amqp.rabbit.listener.api`. - -===== Publisher Confirms Changes - -Channels enabled for publisher confirmations are not returned to the cache while there are outstanding confirmations. -See <> for more information. - -===== Listener Container Factory Improvements - -You can now use the listener container factories to create any listener container, not only those for use with `@RabbitListener` annotations or the `@RabbitListenerEndpointRegistry`. -See <> for more information. - -`ChannelAwareMessageListener` now inherits from `MessageListener`. - -===== Broker Event Listener - -A `BrokerEventListener` is introduced to publish selected broker events as `ApplicationEvent` instances. -See <> for more information. - -===== RabbitAdmin Changes - -The `RabbitAdmin` discovers beans of type `Declarables` (which is a container for `Declarable` - `Queue`, `Exchange`, and `Binding` objects) and declare the contained objects on the broker. -Users are discouraged from using the old mechanism of declaring `>` (and others) and should use `Declarables` beans instead. -By default, the old mechanism is disabled. -See <> for more information. - -`AnonymousQueue` instances are now declared with `x-queue-master-locator` set to `client-local` by default, to ensure the queues are created on the node the application is connected to. -See <> for more information. - -===== RabbitTemplate Changes - -You can now configure the `RabbitTemplate` with the `noLocalReplyConsumer` option to control a `noLocal` flag for reply consumers in the `sendAndReceive()` operations. -See <> for more information. - -`CorrelationData` for publisher confirmations now has a `ListenableFuture`, which you can use to get the acknowledgment instead of using a callback. -When returns and confirmations are enabled, the correlation data, if provided, is populated with the returned message. -See <> for more information. - -A method called `replyTimedOut` is now provided to notify subclasses that a reply has timed out, allowing for any state cleanup. -See <> for more information. - -You can now specify an `ErrorHandler` to be invoked when using request/reply with a `DirectReplyToMessageListenerContainer` (the default) when exceptions occur when replies are delivered (for example, late replies). -See `setReplyErrorHandler` on the `RabbitTemplate`. -(Also since 2.0.11). - -===== Message Conversion - -We introduced a new `Jackson2XmlMessageConverter` to support converting messages from and to XML format. -See <> for more information. - -===== Management REST API - -The `RabbitManagementTemplate` is now deprecated in favor of the direct `com.rabbitmq.http.client.Client` (or `com.rabbitmq.http.client.ReactorNettyClient`) usage. -See <> for more information. - -===== `@RabbitListener` Changes - -The listener container factory can now be configured with a `RetryTemplate` and, optionally, a `RecoveryCallback` used when sending replies. -See <> for more information. - -===== Async `@RabbitListener` Return - -`@RabbitListener` methods can now return `ListenableFuture` or `Mono`. -See <> for more information. - -===== Connection Factory Bean Changes - -By default, the `RabbitConnectionFactoryBean` now calls `enableHostnameVerification()`. -To revert to the previous behavior, set the `enableHostnameVerification` property to `false`. - -===== Connection Factory Changes - -The `CachingConnectionFactory` now unconditionally disables auto-recovery in the underlying RabbitMQ `ConnectionFactory`, even if a pre-configured instance is provided in a constructor. -While steps have been taken to make Spring AMQP compatible with auto recovery, certain corner cases have arisen where issues remain. -Spring AMQP has had its own recovery mechanism since 1.0.0 and does not need to use the recovery provided by the client. -While it is still possible to enable the feature (using `cachingConnectionFactory.getRabbitConnectionFactory()` `.setAutomaticRecoveryEnabled()`) after the `CachingConnectionFactory` is constructed, **we strongly recommend that you not do so**. -We recommend that you use a separate RabbitMQ `ConnectionFactory` if you need auto recovery connections when using the client factory directly (rather than using Spring AMQP components). - -===== Listener Container Changes - -The default `ConditionalRejectingErrorHandler` now completely discards messages that cause fatal errors if an `x-death` header is present. -See <> for more information. - -===== Immediate requeue - -A new `ImmediateRequeueAmqpException` is introduced to notify a listener container that the message has to be re-queued. -To use this feature, a new `ImmediateRequeueMessageRecoverer` implementation is added. - -See <> for more information. - - -==== Changes in 2.0 Since 1.7 - -===== Using `CachingConnectionFactory` - -Starting with version 2.0.2, you can configure the `RabbitTemplate` to use a different connection to that used by listener containers. -This change avoids deadlocked consumers when producers are blocked for any reason. -See <> for more information. - -===== AMQP Client library - -Spring AMQP now uses the new 5.0.x version of the `amqp-client` library provided by the RabbitMQ team. -This client has auto recovery configured by default. -See <>. - -NOTE: As of version 4.0, the client enables automatic recovery by default. -While compatible with this feature, Spring AMQP has its own recovery mechanisms, and the client recovery feature generally is not needed. -We recommend that you disable `amqp-client` automatic recovery, to avoid getting `AutoRecoverConnectionNotCurrentlyOpenException` instances when the broker is available but the connection has not yet recovered. -Starting with version 1.7.1, Spring AMQP disables it unless you explicitly create your own RabbitMQ connection factory and provide it to the `CachingConnectionFactory`. -RabbitMQ `ConnectionFactory` instances created by the `RabbitConnectionFactoryBean` also have the option disabled by default. - -===== General Changes - -The `ExchangeBuilder` now builds durable exchanges by default. -The `@Exchange` annotation used within a `@QeueueBinding` also declares durable exchanges by default. -The `@Queue` annotation used within a `@RabbitListener` by default declares durable queues if named and non-durable if anonymous. -See <> and <> for more information. - -===== Deleted Classes - -`UniquelyNameQueue` is no longer provided. -It is unusual to create a durable non-auto-delete queue with a unique name. -This class has been deleted. -If you require its functionality, use `new Queue(UUID.randomUUID().toString())`. - -===== New Listener Container - -The `DirectMessageListenerContainer` has been added alongside the existing `SimpleMessageListenerContainer`. -See <> and <> for information about choosing which container to use as well as how to configure them. - - -===== Log4j Appender - -This appender is no longer available due to the end-of-life of log4j. -See <> for information about the available log appenders. - - -===== `RabbitTemplate` Changes - -IMPORTANT: Previously, a non-transactional `RabbitTemplate` participated in an existing transaction if it ran on a transactional listener container thread. -This was a serious bug. -However, users might have relied on this behavior. -Starting with version 1.6.2, you must set the `channelTransacted` boolean on the template for it to participate in the container transaction. - -The `RabbitTemplate` now uses a `DirectReplyToMessageListenerContainer` (by default) instead of creating a new consumer for each request. -See <> for more information. - -The `AsyncRabbitTemplate` now supports direct reply-to. -See <> for more information. - -The `RabbitTemplate` and `AsyncRabbitTemplate` now have `receiveAndConvert` and `convertSendAndReceiveAsType` methods that take a `ParameterizedTypeReference` argument, letting the caller specify the type to which to convert the result. -This is particularly useful for complex types or when type information is not conveyed in message headers. -It requires a `SmartMessageConverter` such as the `Jackson2JsonMessageConverter`. -See <>, <>, <>, and <> for more information. - -You can now use a `RabbitTemplate` to perform multiple operations on a dedicated channel. -See <> for more information. - -===== Listener Adapter - -A convenient `FunctionalInterface` is available for using lambdas with the `MessageListenerAdapter`. -See <> for more information. - -===== Listener Container Changes - -====== Prefetch Default Value - -The prefetch default value used to be 1, which could lead to under-utilization of efficient consumers. -The default prefetch value is now 250, which should keep consumers busy in most common scenarios and, -thus, improve throughput. - -IMPORTANT: There are scenarios where the prefetch value should -be low -- for example, with large messages, especially if the processing is slow (messages could add up -to a large amount of memory in the client process), and if strict message ordering is necessary -(the prefetch value should be set back to 1 in this case). -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. - -For more background about prefetch, see this post about https://www.rabbitmq.com/blog/2014/04/14/finding-bottlenecks-with-rabbitmq-3-3/[consumer utilization in RabbitMQ] -and this post about https://www.rabbitmq.com/blog/2012/05/11/some-queuing-theory-throughput-latency-and-bandwidth/[queuing theory]. - -====== Message Count - -Previously, `MessageProperties.getMessageCount()` returned `0` for messages emitted by the container. -This property applies only when you use `basicGet` (for example, from `RabbitTemplate.receive()` methods) and is now initialized to `null` for container messages. - -====== Transaction Rollback Behavior - -Message re-queue on transaction rollback is now consistent, regardless of whether or not a transaction manager is configured. -See <> for more information. - -====== Shutdown Behavior - -If the container threads do not respond to a shutdown within `shutdownTimeout`, the channels are forced closed by default. -See <> for more information. - -====== After Receive Message Post Processors - -If a `MessagePostProcessor` in the `afterReceiveMessagePostProcessors` property returns `null`, the message is discarded (and acknowledged if appropriate). - -===== Connection Factory Changes - -The connection and channel listener interfaces now provide a mechanism to obtain information about exceptions. -See <> and <> for more information. - -A new `ConnectionNameStrategy` is now provided to populate the application-specific identification of the target RabbitMQ connection from the `AbstractConnectionFactory`. -See <> for more information. - -===== Retry Changes - -The `MissingMessageIdAdvice` is no longer provided. -Its functionality is now built-in. -See <> for more information. - -===== Anonymous Queue Naming - -By default, `AnonymousQueues` are now named with the default `Base64UrlNamingStrategy` instead of a simple `UUID` string. -See <> for more information. - -===== `@RabbitListener` Changes - -You can now provide simple queue declarations (bound only to the default exchange) in `@RabbitListener` annotations. -See <> for more information. - -You can now configure `@RabbitListener` annotations so that any exceptions are returned to the sender. -You can also configure a `RabbitListenerErrorHandler` to handle exceptions. -See <> for more information. - -You can now bind a queue with multiple routing keys when you use the `@QueueBinding` annotation. -Also `@QueueBinding.exchange()` now supports custom exchange types and declares durable exchanges by default. - -You can now set the `concurrency` of the listener container at the annotation level rather than having to configure a different container factory for different concurrency settings. - -You can now set the `autoStartup` property of the listener container at the annotation level, overriding the default setting in the container factory. - -You can now set after receive and before send (reply) `MessagePostProcessor` instances in the `RabbitListener` container factories. - -See <> for more information. - -Starting with version 2.0.3, one of the `@RabbitHandler` annotations on a class-level `@RabbitListener` can be designated as the default. -See <> for more information. - -===== Container Conditional Rollback - -When using an external transaction manager (such as JDBC), rule-based rollback is now supported when you provide the container with a transaction attribute. -It is also now more flexible when you use a transaction advice. -See <> for more information. - -===== Remove Jackson 1.x support - -Deprecated in previous versions, Jackson `1.x` converters and related components have now been deleted. -You can use similar components based on Jackson 2.x. -See <> for more information. - -===== JSON Message Converter - -When the `__TypeId__` is set to `Hashtable` for an inbound JSON message, the default conversion type is now `LinkedHashMap`. -Previously, it was `Hashtable`. -To revert to a `Hashtable`, you can use `setDefaultMapType` on the `DefaultClassMapper`. - -===== XML Parsers - -When parsing `Queue` and `Exchange` XML components, the parsers no longer register the `name` attribute value as a bean alias if an `id` attribute is present. -See <> for more information. - -===== Blocked Connection -You can now inject the `com.rabbitmq.client.BlockedListener` into the `org.springframework.amqp.rabbit.connection.Connection` object. -Also, the `ConnectionBlockedEvent` and `ConnectionUnblockedEvent` events are emitted by the `ConnectionFactory` when the connection is blocked or unblocked by the Broker. - -See <> for more information. - -==== Changes in 1.7 Since 1.6 - -===== AMQP Client library - -Spring AMQP now uses the new 4.0.x version of the `amqp-client` library provided by the RabbitMQ team. -This client has auto-recovery configured by default. -See <>. - -NOTE: The 4.0.x client enables automatic recovery by default. -While compatible with this feature, Spring AMQP has its own recovery mechanisms, and the client recovery feature generally is not needed. -We recommend disabling `amqp-client` automatic recovery, to avoid getting `AutoRecoverConnectionNotCurrentlyOpenException` instances when the broker is available but the connection has not yet recovered. -Starting with version 1.7.1, Spring AMQP disables it unless you explicitly create your own RabbitMQ connection factory and provide it to the `CachingConnectionFactory`. -RabbitMQ `ConnectionFactory` instances created by the `RabbitConnectionFactoryBean` also have the option disabled by default. - - -===== Log4j 2 upgrade -The minimum Log4j 2 version (for the `AmqpAppender`) is now `2.7`. -The framework is no longer compatible with previous versions. -See <> for more information. - -===== Logback Appender - -This appender no longer captures caller data (method, line number) by default. -You can re-enable it by setting the `includeCallerData` configuration option. -See <> for information about the available log appenders. - -===== Spring Retry Upgrade - -The minimum Spring Retry version is now `1.2`. -The framework is no longer compatible with previous versions. - -====== Shutdown Behavior - -You can now set `forceCloseChannel` to `true` so that, if the container threads do not respond to a shutdown within `shutdownTimeout`, the channels are forced closed, -causing any unacked messages to be re-queued. -See <> for more information. - -===== FasterXML Jackson upgrade - -The minimum Jackson version is now `2.8`. -The framework is no longer compatible with previous versions. - -===== JUnit `@Rules` - -Rules that have previously been used internally by the framework have now been made available in a separate jar called `spring-rabbit-junit`. -See <> for more information. - -===== Container Conditional Rollback - -When you use an external transaction manager (such as JDBC), rule-based rollback is now supported when you provide the container with a transaction attribute. -It is also now more flexible when you use a transaction advice. - -===== Connection Naming Strategy - -A new `ConnectionNameStrategy` is now provided to populate the application-specific identification of the target RabbitMQ connection from the `AbstractConnectionFactory`. -See <> for more information. - -===== Listener Container Changes - -====== Transaction Rollback Behavior - -You can now configure message re-queue on transaction rollback to be consistent, regardless of whether or not a transaction manager is configured. -See <> for more information. - -==== Earlier Releases - -See <> for changes in previous versions. - -==== Changes in 1.6 Since 1.5 - -===== Testing Support - -A new testing support library is now provided. -See <> for more information. - -===== Builder - -Builders that provide a fluent API for configuring `Queue` and `Exchange` objects are now available. -See <> for more information. - -===== Namespace Changes - -====== Connection Factory - -You can now add a `thread-factory` to a connection factory bean declaration -- for example, to name the threads -created by the `amqp-client` library. -See <> for more information. - -When you use `CacheMode.CONNECTION`, you can now limit the total number of connections allowed. -See <> for more information. - -====== Queue Definitions - -You can now provide a naming strategy for anonymous queues. -See <> for more information. - -===== Listener Container Changes - -====== Idle Message Listener Detection - -You can now configure listener containers to publish `ApplicationEvent` instances when idle. -See <> for more information. - -====== Mismatched Queue Detection - -By default, when a listener container starts, if queues with mismatched properties or arguments are detected, -the container logs the exception but continues to listen. -The container now has a property called `mismatchedQueuesFatal`, which prevents the container (and context) from -starting if the problem is detected during startup. -It also stops the container if the problem is detected later, such as after recovering from a connection failure. -See <> for more information. - -====== Listener Container Logging - -Now, listener container provides its `beanName` to the internal `SimpleAsyncTaskExecutor` as a `threadNamePrefix`. -It is useful for logs analysis. - -====== Default Error Handler - -The default error handler (`ConditionalRejectingErrorHandler`) now considers irrecoverable `@RabbitListener` -exceptions as fatal. -See <> for more information. - - -===== `AutoDeclare` and `RabbitAdmin` Instances - -See <> (`autoDeclare`) for some changes to the semantics of that option with respect to the use -of `RabbitAdmin` instances in the application context. - -===== `AmqpTemplate`: Receive with Timeout - -A number of new `receive()` methods with `timeout` have been introduced for the `AmqpTemplate` -and its `RabbitTemplate` implementation. -See <> for more information. - -===== Using `AsyncRabbitTemplate` - -A new `AsyncRabbitTemplate` has been introduced. -This template provides a number of send and receive methods, where the return value is a `ListenableFuture`, which can -be used later to obtain the result either synchronously or asynchronously. -See <> for more information. - -===== `RabbitTemplate` Changes - -1.4.1 introduced the ability to use https://www.rabbitmq.com/direct-reply-to.html[direct reply-to] when the broker supports it. -It is more efficient than using a temporary queue for each reply. -This version lets you override this default behavior and use a temporary queue by setting the `useTemporaryReplyQueues` property to `true`. -See <> for more information. - -The `RabbitTemplate` now supports a `user-id-expression` (`userIdExpression` when using Java configuration). -See https://www.rabbitmq.com/validated-user-id.html[Validated User-ID RabbitMQ documentation] and <> for more information. - -===== Message Properties - -====== Using `CorrelationId` - -The `correlationId` message property can now be a `String`. -See <> for more information. - -====== Long String Headers - -Previously, the `DefaultMessagePropertiesConverter` "`converted`" headers longer than the long string limit (default 1024) -to a `DataInputStream` (actually, it referenced the `LongString` instance's `DataInputStream`). -On output, this header was not converted (except to a String -- for example, `java.io.DataInputStream@1d057a39` by calling -`toString()` on the stream). - -With this release, long `LongString` instances are now left as `LongString` instances by default. -You can access the contents by using the `getBytes[]`, `toString()`, or `getStream()` methods. -A large incoming `LongString` is now correctly "`converted`" on output too. - -See <> for more information. - -====== Inbound Delivery Mode - -The `deliveryMode` property is no longer mapped to the `MessageProperties.deliveryMode`. -This change avoids unintended propagation if the the same `MessageProperties` object is used to send an outbound message. -Instead, the inbound `deliveryMode` header is mapped to `MessageProperties.receivedDeliveryMode`. - -See <> for more information. - -When using annotated endpoints, the header is provided in the header named `AmqpHeaders.RECEIVED_DELIVERY_MODE`. - -See <> for more information. - -====== Inbound User ID - -The `user_id` property is no longer mapped to the `MessageProperties.userId`. -This change avoids unintended propagation if the the same `MessageProperties` object is used to send an outbound message. -Instead, the inbound `userId` header is mapped to `MessageProperties.receivedUserId`. - -See <> for more information. - -When you use annotated endpoints, the header is provided in the header named `AmqpHeaders.RECEIVED_USER_ID`. - -See <> for more information. - -===== `RabbitAdmin` Changes - -====== Declaration Failures - -Previously, the `ignoreDeclarationFailures` flag took effect only for `IOException` on the channel (such as mis-matched -arguments). -It now takes effect for any exception (such as `TimeoutException`). -In addition, a `DeclarationExceptionEvent` is now published whenever a declaration fails. -The `RabbitAdmin` last declaration event is also available as a property `lastDeclarationExceptionEvent`. -See <> for more information. - -===== `@RabbitListener` Changes - -====== Multiple Containers for Each Bean - -When you use Java 8 or later, you can now add multiple `@RabbitListener` annotations to `@Bean` classes or -their methods. -When using Java 7 or earlier, you can use the `@RabbitListeners` container annotation to provide the same -functionality. -See <> for more information. - -====== `@SendTo` SpEL Expressions - -`@SendTo` for routing replies with no `replyTo` property can now be SpEL expressions evaluated against the -request/reply. -See <> for more information. - -====== `@QueueBinding` Improvements - -You can now specify arguments for queues, exchanges, and bindings in `@QueueBinding` annotations. -Header exchanges are now supported by `@QueueBinding`. -See <> for more information. - -===== Delayed Message Exchange - -Spring AMQP now has first class support for the RabbitMQ Delayed Message Exchange plugin. -See <> for more information. - -===== Exchange Internal Flag - -Any `Exchange` definitions can now be marked as `internal`, and `RabbitAdmin` passes the value to the broker when -declaring the exchange. -See <> for more information. - -===== `CachingConnectionFactory` Changes - -====== `CachingConnectionFactory` Cache Statistics - -The `CachingConnectionFactory` now provides cache properties at runtime and over JMX. -See <> for more information. - -====== Accessing the Underlying RabbitMQ Connection Factory - -A new getter has been added to provide access to the underlying factory. -You can use this getter, for example, to add custom connection properties. -See <> for more information. - -====== Channel Cache - -The default channel cache size has been increased from 1 to 25. -See <> for more information. - -In addition, the `SimpleMessageListenerContainer` no longer adjusts the cache size to be at least as large as the number -of `concurrentConsumers` -- this was superfluous, since the container consumer channels are never cached. - -===== Using `RabbitConnectionFactoryBean` - -The factory bean now exposes a property to add client connection properties to connections made by the resulting -factory. - -===== Java Deserialization - -You can now configure a "`allowed list`" of allowable classes when you use Java deserialization. -You should consider creating an allowed list if you accept messages with serialized java objects from -untrusted sources. -See <> for more information. - -===== JSON `MessageConverter` - -Improvements to the JSON message converter now allow the consumption of messages that do not have type information -in message headers. -See <> and <> for more information. - -===== Logging Appenders - -====== Log4j 2 - -A log4j 2 appender has been added, and the appenders can now be configured with an `addresses` property to connect -to a broker cluster. - -====== Client Connection Properties - -You can now add custom client connection properties to RabbitMQ connections. - -See <> for more information. - -==== Changes in 1.5 Since 1.4 - -===== `spring-erlang` Is No Longer Supported - -The `spring-erlang` jar is no longer included in the distribution. -Use <> instead. - -===== `CachingConnectionFactory` Changes - -====== Empty Addresses Property in `CachingConnectionFactory` - -Previously, if the connection factory was configured with a host and port but an empty String was also supplied for -`addresses`, the host and port were ignored. -Now, an empty `addresses` String is treated the same as a `null`, and the host and port are used. - -====== URI Constructor - -The `CachingConnectionFactory` has an additional constructor, with a `URI` parameter, to configure the broker connection. - -====== Connection Reset - -A new method called `resetConnection()` has been added to let users reset the connection (or connections). -You might use this, for example, to reconnect to the primary broker after failing over to the secondary broker. -This *does* impact in-process operations. -The existing `destroy()` method does exactly the same, but the new method has a less daunting name. - -===== Properties to Control Container Queue Declaration Behavior - -When the listener container consumers start, they attempt to passively declare the queues to ensure they are available -on the broker. -Previously, if these declarations failed (for example, because the queues didn't exist) or when an HA queue was being -moved, the retry logic was fixed at three retry attempts at five-second intervals. -If the queues still do not exist, the behavior is controlled by the `missingQueuesFatal` property (default: `true`). -Also, for containers configured to listen from multiple queues, if only a subset of queues are available, the consumer -retried the missing queues on a fixed interval of 60 seconds. - -The `declarationRetries`, `failedDeclarationRetryInterval`, and `retryDeclarationInterval` properties are now configurable. -See <> for more information. - -===== Class Package Change - -The `RabbitGatewaySupport` class has been moved from `o.s.amqp.rabbit.core.support` to `o.s.amqp.rabbit.core`. - -===== `DefaultMessagePropertiesConverter` Changes - -You can now configure the `DefaultMessagePropertiesConverter` to -determine the maximum length of a `LongString` that is converted -to a `String` rather than to a `DataInputStream`. -The converter has an alternative constructor that takes the value as a limit. -Previously, this limit was hard-coded at `1024` bytes. -(Also available in 1.4.4). - -===== `@RabbitListener` Improvements - -====== `@QueueBinding` for `@RabbitListener` - -The `bindings` attribute has been added to the `@RabbitListener` annotation as mutually exclusive with the `queues` -attribute to allow the specification of the `queue`, its `exchange`, and `binding` for declaration by a `RabbitAdmin` on -the Broker. - -====== SpEL in `@SendTo` - -The default reply address (`@SendTo`) for a `@RabbitListener` can now be a SpEL expression. - -====== Multiple Queue Names through Properties - -You can now use a combination of SpEL and property placeholders to specify multiple queues for a listener. - -See <> for more information. - -===== Automatic Exchange, Queue, and Binding Declaration - -You can now declare beans that define a collection of these entities, and the `RabbitAdmin` adds the -contents to the list of entities that it declares when a connection is established. -See <> for more information. - -===== `RabbitTemplate` Changes - -====== `reply-address` Added - -The `reply-address` attribute has been added to the `` component as an alternative `reply-queue`. -See <> for more information. -(Also available in 1.4.4 as a setter on the `RabbitTemplate`). - -====== Blocking `receive` Methods - -The `RabbitTemplate` now supports blocking in `receive` and `convertAndReceive` methods. -See <> for more information. - -====== Mandatory with `sendAndReceive` Methods - -When the `mandatory` flag is set when using the `sendAndReceive` and `convertSendAndReceive` methods, the calling thread -throws an `AmqpMessageReturnedException` if the request message cannot be delivered. -See <> for more information. - -====== Improper Reply Listener Configuration - -The framework tries to verify proper configuration of a reply listener container when using a named reply queue. - -See <> for more information. - -===== `RabbitManagementTemplate` Added - -The `RabbitManagementTemplate` has been introduced to monitor and configure the RabbitMQ Broker by using the REST API provided by its https://www.rabbitmq.com/management.html[management plugin]. -See <> for more information. - -===== Listener Container Bean Names (XML) - -[IMPORTANT] -==== -The `id` attribute on the `` element has been removed. -Starting with this release, the `id` on the `` child element is used alone to name the listener container bean created for each listener element. - -Normal Spring bean name overrides are applied. -If a later `` is parsed with the same `id` as an existing bean, the new definition overrides the existing one. -Previously, bean names were composed from the `id` attributes of the `` and `` elements. - -When migrating to this release, if you have `id` attributes on your `` elements, remove them and set the `id` on the child `` element instead. -==== - -However, to support starting and stopping containers as a group, a new `group` attribute has been added. -When this attribute is defined, the containers created by this element are added to a bean with this name, of type `Collection`. -You can iterate over this group to start and stop containers. - -===== Class-Level `@RabbitListener` - -The `@RabbitListener` annotation can now be applied at the class level. -Together with the new `@RabbitHandler` method annotation, this lets you select the handler method based on payload type. -See <> for more information. - -===== `SimpleMessageListenerContainer`: BackOff Support - -The `SimpleMessageListenerContainer` can now be supplied with a `BackOff` instance for `consumer` startup recovery. -See <> for more information. - -===== Channel Close Logging - -A mechanism to control the log levels of channel closure has been introduced. -See <>. - -===== Application Events - -The `SimpleMessageListenerContainer` now emits application events when consumers fail. -See <> for more information. - -===== Consumer Tag Configuration - -Previously, the consumer tags for asynchronous consumers were generated by the broker. -With this release, it is now possible to supply a naming strategy to the listener container. -See <>. - -===== Using `MessageListenerAdapter` - -The `MessageListenerAdapter` now supports a map of queue names (or consumer tags) to method names, to determine -which delegate method to call based on the queue from which the message was received. - -===== `LocalizedQueueConnectionFactory` Added - -`LocalizedQueueConnectionFactory` is a new connection factory that connects to the node in a cluster where a mirrored queue actually resides. - -See <>. - -===== Anonymous Queue Naming - -Starting with version 1.5.3, you can now control how `AnonymousQueue` names are generated. -See <> for more information. - - -==== Changes in 1.4 Since 1.3 - -===== `@RabbitListener` Annotation - -POJO listeners can be annotated with `@RabbitListener`, enabled by `@EnableRabbit` or ``. -Spring Framework 4.1 is required for this feature. -See <> for more information. - -===== `RabbitMessagingTemplate` Added - -A new `RabbitMessagingTemplate` lets you interact with RabbitMQ by using `spring-messaging` `Message` instances. -Internally, it uses the `RabbitTemplate`, which you can configure as normal. -Spring Framework 4.1 is required for this feature. -See <> for more information. - -===== Listener Container `missingQueuesFatal` Attribute - -1.3.5 introduced the `missingQueuesFatal` property on the `SimpleMessageListenerContainer`. -This is now available on the listener container namespace element. -See <>. - -===== RabbitTemplate `ConfirmCallback` Interface - -The `confirm` method on this interface has an additional parameter called `cause`. -When available, this parameter contains the reason for a negative acknowledgement (nack). -See <>. - -===== `RabbitConnectionFactoryBean` Added - -`RabbitConnectionFactoryBean` creates the underlying RabbitMQ `ConnectionFactory` used by the `CachingConnectionFactory`. -This enables configuration of SSL options using Spring's dependency injection. -See <>. - -===== Using `CachingConnectionFactory` - -The `CachingConnectionFactory` now lets the `connectionTimeout` be set as a property or as an attribute in the namespace. -It sets the property on the underlying RabbitMQ `ConnectionFactory`. -See <>. - -===== Log Appender - -The Logback `org.springframework.amqp.rabbit.logback.AmqpAppender` has been introduced. -It provides options similar to `org.springframework.amqp.rabbit.log4j.AmqpAppender`. -For more information, see the JavaDoc of these classes. - -The Log4j `AmqpAppender` now supports the `deliveryMode` property (`PERSISTENT` or `NON_PERSISTENT`, default: `PERSISTENT`). -Previously, all log4j messages were `PERSISTENT`. - -The appender also supports modification of the `Message` before sending -- allowing, for example, the addition of custom headers. -Subclasses should override the `postProcessMessageBeforeSend()`. - -===== Listener Queues - -The listener container now, by default, redeclares any missing queues during startup. -A new `auto-declare` attribute has been added to the `` to prevent these re-declarations. -See <>. - -===== `RabbitTemplate`: `mandatory` and `connectionFactorySelector` Expressions - -The `mandatoryExpression`, `sendConnectionFactorySelectorExpression`, and `receiveConnectionFactorySelectorExpression` SpEL Expression`s properties have been added to `RabbitTemplate`. -The `mandatoryExpression` is used to evaluate a `mandatory` boolean value against each request message when a `ReturnCallback` is in use. -See <>. -The `sendConnectionFactorySelectorExpression` and `receiveConnectionFactorySelectorExpression` are used when an `AbstractRoutingConnectionFactory` is provided, to determine the `lookupKey` for the target `ConnectionFactory` at runtime on each AMQP protocol interaction operation. -See <>. - -===== Listeners and the Routing Connection Factory - -You can configure a `SimpleMessageListenerContainer` with a routing connection factory to enable connection selection based on the queue names. -See <>. - -===== `RabbitTemplate`: `RecoveryCallback` Option - -The `recoveryCallback` property has been added for use in the `retryTemplate.execute()`. -See <>. - -===== `MessageConversionException` Change - -This exception is now a subclass of `AmqpException`. -Consider the following code: - -==== -[source,java] ----- -try { - template.convertAndSend("thing1", "thing2", "cat"); -} -catch (AmqpException e) { - ... -} -catch (MessageConversionException e) { - ... -} ----- -==== - -The second catch block is no longer reachable and needs to be moved above the catch-all `AmqpException` catch block. - -===== RabbitMQ 3.4 Compatibility - -Spring AMQP is now compatible with the RabbitMQ 3.4, including direct reply-to. -See <> and <> for more information. - -===== `ContentTypeDelegatingMessageConverter` Added - -The `ContentTypeDelegatingMessageConverter` has been introduced to select the `MessageConverter` to use, based on the `contentType` property in the `MessageProperties`. -See <> for more information. - -==== Changes in 1.3 Since 1.2 - -===== Listener Concurrency - -The listener container now supports dynamic scaling of the number of consumers based on workload, or you can programmatically change the concurrency without stopping the container. -See <>. - -===== Listener Queues - -The listener container now permits the queues on which it listens to be modified at runtime. -Also, the container now starts if at least one of its configured queues is available for use. -See <> - -This listener container now redeclares any auto-delete queues during startup. -See <>. - -===== Consumer Priority - -The listener container now supports consumer arguments, letting the `x-priority` argument be set. -See <>. - -===== Exclusive Consumer - -You can now configure `SimpleMessageListenerContainer` with a single `exclusive` consumer, preventing other consumers from listening to the queue. -See <>. - -===== Rabbit Admin - -You can now have the broker generate the queue name, regardless of `durable`, `autoDelete`, and `exclusive` settings. -See <>. - -===== Direct Exchange Binding - -Previously, omitting the `key` attribute from a `binding` element of a `direct-exchange` configuration caused the queue or exchange to be bound with an empty string as the routing key. -Now it is bound with the the name of the provided `Queue` or `Exchange`. -If you wish to bind with an empty string routing key, you need to specify `key=""`. - -===== `AmqpTemplate` Changes - -The `AmqpTemplate` now provides several synchronous `receiveAndReply` methods. -These are implemented by the `RabbitTemplate`. -For more information see <>. - -The `RabbitTemplate` now supports configuring a `RetryTemplate` to attempt retries (with optional back-off policy) for when the broker is not available. -For more information see <>. - -===== Caching Connection Factory - -You can now configure the caching connection factory to cache `Connection` instances and their `Channel` instances instead of using a single connection and caching only `Channel` instances. -See <>. - -===== Binding Arguments - -The `` of the `` now supports parsing of the `` sub-element. -You can now configure the `` of the `` with a `key/value` attribute pair (to match on a single header) or with a `` sub-element (allowing matching on multiple headers). -These options are mutually exclusive. -See <>. - -===== Routing Connection Factory - -A new `SimpleRoutingConnectionFactory` has been introduced. -It allows configuration of `ConnectionFactories` mapping, to determine the target `ConnectionFactory` to use at runtime. -See <>. - -===== `MessageBuilder` and `MessagePropertiesBuilder` - -"`Fluent APIs`" for building messages or message properties are now provided. -See <>. - -===== `RetryInterceptorBuilder` Change - -A "`Fluent API`" for building listener container retry interceptors is now provided. -See <>. - -===== `RepublishMessageRecoverer` Added - -This new `MessageRecoverer` is provided to allow publishing a failed message to another queue (including stack trace information in the header) when retries are exhausted. -See <>. - -===== Default Error Handler (Since 1.3.2) - -A default `ConditionalRejectingErrorHandler` has been added to the listener container. -This error handler detects fatal message conversion problems and instructs the container to reject the message to prevent the broker from continually redelivering the unconvertible message. -See <>. - -===== Listener Container 'missingQueuesFatal` Property (Since 1.3.5) - -The `SimpleMessageListenerContainer` now has a property called `missingQueuesFatal` (default: `true`). -Previously, missing queues were always fatal. -See <>. - -==== Changes to 1.2 Since 1.1 - -===== RabbitMQ Version - -Spring AMQP now uses RabbitMQ 3.1.x by default (but retains compatibility with earlier versions). -Certain deprecations have been added for features no longer supported by RabbitMQ 3.1.x -- federated exchanges and the `immediate` property on the `RabbitTemplate`. - -===== Rabbit Admin - -`RabbitAdmin` now provides an option to let exchange, queue, and binding declarations continue when a declaration fails. -Previously, all declarations stopped on a failure. -By setting `ignore-declaration-exceptions`, such exceptions are logged (at the `WARN` level), but further declarations continue. -An example where this might be useful is when a queue declaration fails because of a slightly different `ttl` setting that would normally stop other declarations from proceeding. - -`RabbitAdmin` now provides an additional method called `getQueueProperties()`. -You can use this determine if a queue exists on the broker (returns `null` for a non-existent queue). -In addition, it returns the current number of messages in the queue as well as the current number of consumers. - -===== Rabbit Template - -Previously, when the `...sendAndReceive()` methods were used with a fixed reply queue, two custom headers were used for correlation data and to retain and restore reply queue information. -With this release, the standard message property (`correlationId`) is used by default, although you can specify a custom property to use instead. -In addition, nested `replyTo` information is now retained internally in the template, instead of using a custom header. - -The `immediate` property is deprecated. -You must not set this property when using RabbitMQ 3.0.x or greater. - -===== JSON Message Converters - -A Jackson 2.x `MessageConverter` is now provided, along with the existing converter that uses Jackson 1.x. - -===== Automatic Declaration of Queues and Other Items - -Previously, when declaring queues, exchanges and bindings, you could not define which connection factory was used for the declarations. -Each `RabbitAdmin` declared all components by using its connection. - -Starting with this release, you can now limit declarations to specific `RabbitAdmin` instances. -See <>. - -===== AMQP Remoting - -Facilities are now provided for using Spring remoting techniques, using AMQP as the transport for the RPC calls. -For more information see <> - -===== Requested Heart Beats - -Several users have asked for the underlying client connection factory's `requestedHeartBeats` property to be exposed on the Spring AMQP `CachingConnectionFactory`. -This is now available. -Previously, it was necessary to configure the AMQP client factory as a separate bean and provide a reference to it in the `CachingConnectionFactory`. - -==== Changes to 1.1 Since 1.0 - -===== General - -Spring-AMQP is now built with Gradle. - -Adds support for publisher confirms and returns. - -Adds support for HA queues and broker failover. - -Adds support for dead letter exchanges and dead letter queues. - -===== AMQP Log4j Appender - -Adds an option to support adding a message ID to logged messages. - -Adds an option to allow the specification of a `Charset` name to be used when converting `String` to `byte[]`. diff --git a/src/reference/asciidoc/docinfo.html b/src/reference/asciidoc/docinfo.html deleted file mode 100644 index 19e2462b2c..0000000000 --- a/src/reference/asciidoc/docinfo.html +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/src/reference/asciidoc/index.adoc b/src/reference/asciidoc/index.adoc deleted file mode 100644 index 71ff75d97b..0000000000 --- a/src/reference/asciidoc/index.adoc +++ /dev/null @@ -1,70 +0,0 @@ -[[spring-amqp-reference]] -= Spring AMQP -ifdef::backend-html5[] -:revnumber: '' -endif::[] -:toc: left -:toclevels: 4 -:numbered: -:icons: font -:hide-uri-scheme: -Mark Pollack; Mark Fisher; Oleg Zhurakousky; Dave Syer; Gary Russell; Gunnar Hillert; Artem Bilan; Stéphane Nicoll; Arnaud Cogoluègnes; Jay Bryant - -ifdef::backend-html5[] -*{project-version}* - -NOTE: This documentation is also available as https://docs.spring.io/spring-amqp/docs/current/reference/pdf/spring-amqp-reference.pdf[PDF]. -endif::[] - -ifdef::backend-pdf[] -NOTE: This documentation is also available as https://docs.spring.io/spring-amqp/docs/current/reference/html/index.html[HTML]. -endif::[] - -(C) 2010 - 2021 by VMware, Inc. - -Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically. - - -== Preface - -include::preface.adoc[] - -include::whats-new.adoc[] - -== Introduction - -This first part of the reference documentation is a high-level overview of Spring AMQP and the underlying concepts. -It includes some code snippets to get you up and running as quickly as possible. - -include::quick-tour.adoc[] - -== Reference - -This part of the reference documentation details the various components that comprise Spring AMQP. -The <> covers the core classes to develop an AMQP application. -This part also includes a chapter about the <>. - -include::amqp.adoc[] - -include::stream.adoc[] - -include::logging.adoc[] - -include::sample-apps.adoc[] - -include::testing.adoc[] - -== Spring Integration - Reference - -This part of the reference documentation provides a quick introduction to the AMQP support within the Spring Integration project. - -include::si-amqp.adoc[] - -[[resources]] -== Other Resources - -In addition to this reference documentation, there exist a number of other resources that may help you learn about AMQP. - -include::further-reading.adoc[] - -include::appendix.adoc[] From 41d786bd965a2acad4423b5d0eab6064caa59516 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 5 Dec 2023 16:44:29 -0500 Subject: [PATCH 317/737] Some docs related cleanups * Remove docs zipping * Remove AsciiDoc plugins and tasks * Fix `integration-reference.adoc` - use the whole content of just remove simple `si-amqp.adoc` * Remove obsolete `ant/upload-dist.xml` --- build.gradle | 87 +------------------ src/ant/upload-dist.xml | 65 -------------- src/reference/antora/antora-playbook.yml | 2 +- src/reference/antora/antora.yml | 2 +- src/reference/antora/modules/ROOT/nav.adoc | 1 - .../antora/modules/ROOT/pages/index.adoc | 5 +- .../ROOT/pages/integration-reference.adoc | 72 ++++++++++++++- .../antora/modules/ROOT/pages/si-amqp.adoc | 74 ---------------- 8 files changed, 76 insertions(+), 232 deletions(-) delete mode 100644 src/ant/upload-dist.xml delete mode 100644 src/reference/antora/modules/ROOT/pages/si-amqp.adoc diff --git a/build.gradle b/build.gradle index ce2a02b674..ca3919b891 100644 --- a/build.gradle +++ b/build.gradle @@ -22,8 +22,6 @@ plugins { id 'org.ajoberstar.grgit' version '4.1.1' id 'io.spring.nohttp' version '0.0.11' id 'io.spring.dependency-management' version '1.1.4' apply false - id 'org.asciidoctor.jvm.pdf' version '3.3.2' - id 'org.asciidoctor.jvm.convert' version '3.3.2' id 'org.antora' version '1.0.0' id 'io.spring.antora.generate-antora-yml' version '0.0.1' } @@ -37,7 +35,6 @@ ext { linkScmUrl = 'https://github.com/spring-projects/spring-amqp' linkScmConnection = 'git://github.com/spring-projects/spring-amqp.git' linkScmDevConnection = 'git@github.com:spring-projects/spring-amqp.git' - springAsciidoctorBackendsVersion = '0.0.7' modifiedFiles = files() @@ -95,7 +92,7 @@ antora { ] } -tasks.named("generateAntoraYml") { +tasks.named('generateAntoraYml') { asciidocAttributes = project.provider( { return ['project-version' : project.version ] } ) @@ -548,27 +545,17 @@ project('spring-rabbit-test') { } configurations { - asciidoctorExtensions micrometerDocs } dependencies { - asciidoctorExtensions "io.spring.asciidoctor.backends:spring-asciidoctor-backends:${springAsciidoctorBackendsVersion}" micrometerDocs "io.micrometer:micrometer-docs-generator:$micrometerDocsVersion" } -task prepareAsciidocBuild(type: Sync) { - dependsOn configurations.asciidoctorExtensions - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - from 'src/reference/asciidoc/' - into "$buildDir/asciidoc" -} - def observationInputDir = file("$buildDir/docs/microsources").absolutePath def generatedDocsDir = file("$buildDir/docs/generated").absolutePath task copyObservation(type: Copy) { - dependsOn prepareAsciidocBuild from file('spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer').absolutePath from file('spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer').absolutePath include '*.java' @@ -594,66 +581,6 @@ task filterMetricsDocsContent(type: Copy) { filter { line -> line.replaceAll('org.springframework.*.micrometer.', '').replaceAll('^Fully qualified n', 'N') } } -asciidoctorPdf { - dependsOn prepareAsciidocBuild, filterMetricsDocsContent - baseDirFollowsSourceFile() - - asciidoctorj { - sourceDir "$buildDir/asciidoc" - inputs.dir(sourceDir).withPathSensitivity(PathSensitivity.RELATIVE) - sources { - include 'index.adoc' - } - options doctype: 'book' - attributes 'icons': 'font', - 'sectanchors': '', - 'sectnums': '', - 'toc': '', - 'source-highlighter' : 'coderay', - revnumber: project.version, - 'project-version': project.version - } -} - -asciidoctor { - dependsOn asciidoctorPdf - baseDirFollowsSourceFile() - sourceDir "$buildDir/asciidoc" - configurations 'asciidoctorExtensions' - outputOptions { - backends "spring-html" - } - resources { - from(sourceDir) { - include 'images/*', 'css/**', 'js/**' - } - } - options doctype: 'book', eruby: 'erubis' - - attributes 'docinfo': 'shared', - stylesdir: "css/", - stylesheet: 'spring.css', - 'linkcss': true, - 'icons': 'font', - 'sectanchors': '', - 'source-highlighter': 'highlight.js', - 'highlightjsdir': 'js/highlight', - 'highlightjs-theme': 'github', - 'idprefix': '', - 'idseparator': '-', - 'spring-version': project.version, - 'allow-uri-read': '', - 'toc': 'left', - 'toclevbels': '4', - revnumber: project.version, - 'project-version': project.version -} - -task reference(dependsOn: asciidoctor) { - group = 'Documentation' - description = 'Generate the reference documentation' -} - task api(type: Javadoc) { group = 'Documentation' description = 'Generates aggregated Javadoc API documentation.' @@ -717,7 +644,7 @@ task schemaZip(type: Zip) { } } -task docsZip(type: Zip, dependsOn: [reference]) { +task docsZip(type: Zip) { group = 'Distribution' archiveClassifier = 'docs' description = "Builds -${archiveClassifier} archive containing api and reference " + @@ -730,16 +657,6 @@ task docsZip(type: Zip, dependsOn: [reference]) { from (api) { into 'api' } - - from ('build/docs/asciidoc') { - into 'reference/html' - } - - from ('build/docs/asciidocPdf') { - include 'index.pdf' - rename 'index.pdf', 'spring-amqp-reference.pdf' - into 'reference/pdf' - } } task distZip(type: Zip, dependsOn: [docsZip, schemaZip]) { diff --git a/src/ant/upload-dist.xml b/src/ant/upload-dist.xml deleted file mode 100644 index be958eb51c..0000000000 --- a/src/ant/upload-dist.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Copying dist .ZIP to ${dist.staging} - - - - - diff --git a/src/reference/antora/antora-playbook.yml b/src/reference/antora/antora-playbook.yml index ca962c0696..999864f16b 100644 --- a/src/reference/antora/antora-playbook.yml +++ b/src/reference/antora/antora-playbook.yml @@ -41,4 +41,4 @@ runtime: format: pretty ui: bundle: - url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.9/ui-bundle.zip + url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.10/ui-bundle.zip diff --git a/src/reference/antora/antora.yml b/src/reference/antora/antora.yml index 6529e29d62..fc3041c337 100644 --- a/src/reference/antora/antora.yml +++ b/src/reference/antora/antora.yml @@ -6,7 +6,7 @@ nav: ext: collector: run: - command: gradlew -q "-Dorg.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError" :generateAntoraResources + command: gradlew -q :generateAntoraResources local: true scan: dir: build/generated-antora-resources diff --git a/src/reference/antora/modules/ROOT/nav.adoc b/src/reference/antora/modules/ROOT/nav.adoc index 9d71ec1ef1..a6a17cb96a 100644 --- a/src/reference/antora/modules/ROOT/nav.adoc +++ b/src/reference/antora/modules/ROOT/nav.adoc @@ -61,7 +61,6 @@ ** xref:sample-apps.adoc[] ** xref:testing.adoc[] * xref:integration-reference.adoc[] -** xref:si-amqp.adoc[] * xref:resources.adoc[] ** xref:further-reading.adoc[] * xref:appendix/micrometer.adoc[] diff --git a/src/reference/antora/modules/ROOT/pages/index.adoc b/src/reference/antora/modules/ROOT/pages/index.adoc index d69ae83140..ab6ddca384 100644 --- a/src/reference/antora/modules/ROOT/pages/index.adoc +++ b/src/reference/antora/modules/ROOT/pages/index.adoc @@ -1,8 +1,5 @@ [[spring-amqp-reference]] = Spring AMQP -ifdef::backend-html5[] -:revnumber: '' -endif::[] :numbered: :icons: font :hide-uri-scheme: @@ -16,6 +13,6 @@ These libraries facilitate management of AMQP resources while promoting the use In all of these cases, you can see similarities to the JMS support in the Spring Framework. For other project-related information, visit the Spring AMQP project https://projects.spring.io/spring-amqp/[homepage]. -(C) 2010 - 2021 by VMware, Inc. +(C) 2010 - 2023 Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically. \ No newline at end of file diff --git a/src/reference/antora/modules/ROOT/pages/integration-reference.adoc b/src/reference/antora/modules/ROOT/pages/integration-reference.adoc index f0d23fe5ea..77745b8955 100644 --- a/src/reference/antora/modules/ROOT/pages/integration-reference.adoc +++ b/src/reference/antora/modules/ROOT/pages/integration-reference.adoc @@ -1,4 +1,74 @@ [[spring-integration-reference]] = Spring Integration - Reference -This part of the reference documentation provides a quick introduction to the AMQP support within the Spring Integration project. \ No newline at end of file +This part of the reference documentation provides a quick introduction to the AMQP support within the Spring Integration project. + +[[spring-integration-amqp-introduction]] +== Introduction + +The https://spring.io/spring-integration[Spring Integration] project includes AMQP Channel Adapters and Gateways that build upon the Spring AMQP project. +Those adapters are developed and released in the Spring Integration project. +In Spring Integration, "`Channel Adapters`" are unidirectional (one-way), whereas "`Gateways`" are bidirectional (request-reply). +We provide an inbound-channel-adapter, an outbound-channel-adapter, an inbound-gateway, and an outbound-gateway. + +Since the AMQP adapters are part of the Spring Integration release, the documentation is available as part of the Spring Integration distribution. +We provide a quick overview of the main features here. +See the https://docs.spring.io/spring-integration/reference[Spring Integration Reference Guide] for much more detail. + +[[inbound-channel-adapter]] +== Inbound Channel Adapter + +To receive AMQP Messages from a queue, you can configure an ``. +The following example shows how to configure an inbound channel adapter: + +[source,xml] +---- + +---- + +[[outbound-channel-adapter]] +== Outbound Channel Adapter + +To send AMQP Messages to an exchange, you can configure an ``. +You can optionally provide a 'routing-key' in addition to the exchange name. +The following example shows how to define an outbound channel adapter: + +[source,xml] +---- + +---- + +[[inbound-gateway]] +== Inbound Gateway + +To receive an AMQP Message from a queue and respond to its reply-to address, you can configure an ``. +The following example shows how to define an inbound gateway: + +[source,xml] +---- + +---- + +[[outbound-gateway]] +== Outbound Gateway + +To send AMQP Messages to an exchange and receive back a response from a remote client, you can configure an ``. +You can optionally provide a 'routing-key' in addition to the exchange name. +The following example shows how to define an outbound gateway: + +[source,xml] +---- + +---- diff --git a/src/reference/antora/modules/ROOT/pages/si-amqp.adoc b/src/reference/antora/modules/ROOT/pages/si-amqp.adoc deleted file mode 100644 index 02d5fbf7b6..0000000000 --- a/src/reference/antora/modules/ROOT/pages/si-amqp.adoc +++ /dev/null @@ -1,74 +0,0 @@ -[[spring-integration-amqp]] -= Spring Integration AMQP Support - -This brief chapter covers the relationship between the Spring Integration and the Spring AMQP projects. - -[[spring-integration-amqp-introduction]] -== Introduction - -The https://www.springsource.org/spring-integration[Spring Integration] project includes AMQP Channel Adapters and Gateways that build upon the Spring AMQP project. -Those adapters are developed and released in the Spring Integration project. -In Spring Integration, "`Channel Adapters`" are unidirectional (one-way), whereas "`Gateways`" are bidirectional (request-reply). -We provide an inbound-channel-adapter, an outbound-channel-adapter, an inbound-gateway, and an outbound-gateway. - -Since the AMQP adapters are part of the Spring Integration release, the documentation is available as part of the Spring Integration distribution. -We provide a quick overview of the main features here. -See the https://docs.spring.io/spring-integration/reference/htmlsingle/[Spring Integration Reference Guide] for much more detail. - -[[inbound-channel-adapter]] -== Inbound Channel Adapter - -To receive AMQP Messages from a queue, you can configure an ``. -The following example shows how to configure an inbound channel adapter: - -[source,xml] ----- - ----- - -[[outbound-channel-adapter]] -== Outbound Channel Adapter - -To send AMQP Messages to an exchange, you can configure an ``. -You can optionally provide a 'routing-key' in addition to the exchange name. -The following example shows how to define an outbound channel adapter: - -[source,xml] ----- - ----- - -[[inbound-gateway]] -== Inbound Gateway - -To receive an AMQP Message from a queue and respond to its reply-to address, you can configure an ``. -The following example shows how to define an inbound gateway: - -[source,xml] ----- - ----- - -[[outbound-gateway]] -== Outbound Gateway - -To send AMQP Messages to an exchange and receive back a response from a remote client, you can configure an ``. -You can optionally provide a 'routing-key' in addition to the exchange name. -The following example shows how to define an outbound gateway: - -[source,xml] ----- - ----- From fe3cf2245c4811403e511e4e7cdeebb03aa497f6 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 6 Dec 2023 13:46:28 -0500 Subject: [PATCH 318/737] Use `libs-release-staging` to verify samples The previous virtual repo `libs-spring-dataflow-private-staging-release` is deprecated. * Use newly created, with more general purpose, `libs-release-staging` --- .github/workflows/verify-staged-artifacts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verify-staged-artifacts.yml b/.github/workflows/verify-staged-artifacts.yml index 77cccbac8d..9ac561d0a0 100644 --- a/.github/workflows/verify-staged-artifacts.yml +++ b/.github/workflows/verify-staged-artifacts.yml @@ -31,7 +31,7 @@ jobs: JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} - name: Configure JFrog Cli - run: jf mvnc --repo-resolve-releases=libs-spring-dataflow-private-staging-release --repo-resolve-snapshots=snapshot + run: jf mvnc --repo-resolve-releases=libs-release-staging --repo-resolve-snapshots=snapshot - name: Verify samples against staged release run: jf mvn verify -D"spring.amqp.version"=${{ inputs.releaseVersion }} -B -ntp From 2510338275a4d28c4846cc943bb3c3bdd6cec5ba Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 7 Dec 2023 14:08:51 -0500 Subject: [PATCH 319/737] Adjust GHAs for back-ports --- .github/workflows/backport-issue.yml | 12 ++++++++++++ .github/workflows/ci-snapshot.yml | 1 + .github/workflows/verify-staged-artifacts.yml | 1 + 3 files changed, 14 insertions(+) create mode 100644 .github/workflows/backport-issue.yml diff --git a/.github/workflows/backport-issue.yml b/.github/workflows/backport-issue.yml new file mode 100644 index 0000000000..59326f48d8 --- /dev/null +++ b/.github/workflows/backport-issue.yml @@ -0,0 +1,12 @@ +name: Backport Issue + +on: + push: + branches: + - '*.x' + +jobs: + backport-issue: + uses: artembilan/spring-github-workflows/.github/workflows/spring-backport-issue.yml@main + secrets: + GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/ci-snapshot.yml b/.github/workflows/ci-snapshot.yml index 92496aacfe..74ed09a830 100644 --- a/.github/workflows/ci-snapshot.yml +++ b/.github/workflows/ci-snapshot.yml @@ -5,6 +5,7 @@ on: push: branches: - main + - '*.x' jobs: build-snapshot: diff --git a/.github/workflows/verify-staged-artifacts.yml b/.github/workflows/verify-staged-artifacts.yml index 9ac561d0a0..b0eb786756 100644 --- a/.github/workflows/verify-staged-artifacts.yml +++ b/.github/workflows/verify-staged-artifacts.yml @@ -17,6 +17,7 @@ jobs: uses: actions/checkout@v4 with: repository: spring-projects/spring-amqp-samples + ref: ${{ github.ref_name }} show-progress: false - name: Set up JDK From b7e77fdf1aab1b6301e733f173ff359b4632b110 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 11 Dec 2023 17:19:26 -0500 Subject: [PATCH 320/737] GH-2516: Introduce `JacksonUtils` Fixes: #2516 * The new `JacksonUtils` registers some well-known Jackson modules. * Use `JacksonUtils.enhancedObjectMapper()` in the `Jackson2JsonMessageConverter` for more straightforward configuration from target projects --- build.gradle | 8 +- .../Jackson2JsonMessageConverter.java | 7 +- .../amqp/support/converter/JacksonUtils.java | 127 ++++++++++++++++++ 3 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 spring-amqp/src/main/java/org/springframework/amqp/support/converter/JacksonUtils.java diff --git a/build.gradle b/build.gradle index ca3919b891..22d021a2ca 100644 --- a/build.gradle +++ b/build.gradle @@ -379,7 +379,13 @@ project('spring-amqp') { optionalApi 'com.fasterxml.jackson.core:jackson-databind' optionalApi 'com.fasterxml.jackson.core:jackson-annotations' optionalApi 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' - + optionalApi 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8' + optionalApi 'com.fasterxml.jackson.module:jackson-module-parameter-names' + optionalApi 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' + optionalApi 'com.fasterxml.jackson.datatype:jackson-datatype-joda' + optionalApi ('com.fasterxml.jackson.module:jackson-module-kotlin') { + exclude group: 'org.jetbrains.kotlin' + } // Spring Data projection message binding support optionalApi ('org.springframework.data:spring-data-commons') { exclude group: 'org.springframework' diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverter.java index 9e1c3c721a..3045ecd013 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -41,6 +41,7 @@ public class Jackson2JsonMessageConverter extends AbstractJackson2MessageConvert * Construct with an internal {@link ObjectMapper} instance * and trusted packed to all ({@code *}). * @since 1.6.11 + * @see JacksonUtils#enhancedObjectMapper() */ public Jackson2JsonMessageConverter() { this("*"); @@ -53,10 +54,10 @@ public Jackson2JsonMessageConverter() { * @param trustedPackages the trusted Java packages for deserialization * @since 1.6.11 * @see DefaultJackson2JavaTypeMapper#setTrustedPackages(String...) + * @see JacksonUtils#enhancedObjectMapper() */ public Jackson2JsonMessageConverter(String... trustedPackages) { - this(new ObjectMapper(), trustedPackages); - this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + this(JacksonUtils.enhancedObjectMapper(), trustedPackages); } /** diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/JacksonUtils.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/JacksonUtils.java new file mode 100644 index 0000000000..f988ed5ba4 --- /dev/null +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/JacksonUtils.java @@ -0,0 +1,127 @@ +/* + * Copyright 2023 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.support.converter; + +import org.springframework.util.ClassUtils; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; + + +/** + * The utilities for Jackson {@link ObjectMapper} instances. + * + * @author Artem Bilan + * + * @since 3.1.1 + */ +public final class JacksonUtils { + + private static final boolean JDK8_MODULE_PRESENT = + ClassUtils.isPresent("com.fasterxml.jackson.datatype.jdk8.Jdk8Module", null); + + private static final boolean PARAMETER_NAMES_MODULE_PRESENT = + ClassUtils.isPresent("com.fasterxml.jackson.module.paramnames.ParameterNamesModule", null); + + private static final boolean JAVA_TIME_MODULE_PRESENT = + ClassUtils.isPresent("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule", null); + + private static final boolean JODA_MODULE_PRESENT = + ClassUtils.isPresent("com.fasterxml.jackson.datatype.joda.JodaModule", null); + + private static final boolean KOTLIN_MODULE_PRESENT = + ClassUtils.isPresent("kotlin.Unit", null) && + ClassUtils.isPresent("com.fasterxml.jackson.module.kotlin.KotlinModule", null); + + /** + * Factory for {@link ObjectMapper} instances with registered well-known modules + * and disabled {@link MapperFeature#DEFAULT_VIEW_INCLUSION} and + * {@link DeserializationFeature#FAIL_ON_UNKNOWN_PROPERTIES} features. + * @return the {@link ObjectMapper} instance. + */ + public static ObjectMapper enhancedObjectMapper() { + ObjectMapper objectMapper = JsonMapper.builder() + .configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .build(); + registerWellKnownModulesIfAvailable(objectMapper); + return objectMapper; + } + + private static void registerWellKnownModulesIfAvailable(ObjectMapper objectMapper) { + if (JDK8_MODULE_PRESENT) { + objectMapper.registerModule(Jdk8ModuleProvider.MODULE); + } + + if (PARAMETER_NAMES_MODULE_PRESENT) { + objectMapper.registerModule(ParameterNamesProvider.MODULE); + } + + if (JAVA_TIME_MODULE_PRESENT) { + objectMapper.registerModule(JavaTimeModuleProvider.MODULE); + } + + if (JODA_MODULE_PRESENT) { + objectMapper.registerModule(JodaModuleProvider.MODULE); + } + + if (KOTLIN_MODULE_PRESENT) { + objectMapper.registerModule(KotlinModuleProvider.MODULE); + } + } + + private JacksonUtils() { + } + + private static final class Jdk8ModuleProvider { + + static final com.fasterxml.jackson.databind.Module MODULE = + new com.fasterxml.jackson.datatype.jdk8.Jdk8Module(); + + } + + private static final class ParameterNamesProvider { + + static final com.fasterxml.jackson.databind.Module MODULE = + new com.fasterxml.jackson.module.paramnames.ParameterNamesModule(); + + } + + private static final class JavaTimeModuleProvider { + + static final com.fasterxml.jackson.databind.Module MODULE = + new com.fasterxml.jackson.datatype.jsr310.JavaTimeModule(); + + } + + private static final class JodaModuleProvider { + + static final com.fasterxml.jackson.databind.Module MODULE = + new com.fasterxml.jackson.datatype.joda.JodaModule(); + + } + + private static final class KotlinModuleProvider { + + static final com.fasterxml.jackson.databind.Module MODULE = + new com.fasterxml.jackson.module.kotlin.KotlinModule.Builder().build(); + + } + +} From c5489e291249d3da7b8c771b673441099ffcc84f Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Mon, 11 Dec 2023 23:22:27 +0100 Subject: [PATCH 321/737] GH-2481: Replace trivial synchronized with locks Fixes: #2481 --- .gitignore | 1 + .../amqp/core/AbstractDeclarable.java | 28 ++- .../listener/StreamListenerContainer.java | 115 +++++++---- .../stream/producer/RabbitStreamTemplate.java | 99 ++++++--- .../amqp/rabbit/test/TestRabbitTemplate.java | 21 +- .../amqp/rabbit/AsyncRabbitTemplate.java | 98 +++++---- .../connection/AbstractConnectionFactory.java | 151 ++++++++------ .../LocalizedQueueConnectionFactory.java | 31 ++- .../PooledChannelConnectionFactory.java | 43 ++-- .../PublisherCallbackChannelImpl.java | 194 ++++++++++++------ .../ThreadChannelConnectionFactory.java | 55 +++-- .../rabbit/core/BatchingRabbitTemplate.java | 59 ++++-- .../amqp/rabbit/core/BrokerEventListener.java | 61 ++++-- .../amqp/rabbit/core/RabbitAdmin.java | 5 + .../amqp/rabbit/log4j2/AmqpAppender.java | 12 +- .../RoutingConnectionFactoryTests.java | 42 +++- 16 files changed, 677 insertions(+), 338 deletions(-) diff --git a/.gitignore b/.gitignore index 3b3bb7540b..3a4003bf50 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ nohup.out src/ant/.ant-targets-upload-dist.xml target .sts4-cache +.vscode diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractDeclarable.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractDeclarable.java index 3f662d1447..83002a2ec3 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractDeclarable.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractDeclarable.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -22,6 +22,9 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -30,11 +33,14 @@ * Base class for {@link Declarable} classes. * * @author Gary Russell + * @author Christian Tzolov * @since 1.2 * */ public abstract class AbstractDeclarable implements Declarable { + private final Lock lock = new ReentrantLock(); + private boolean shouldDeclare = true; private Collection declaringAdmins = new ArrayList(); @@ -109,13 +115,25 @@ public void setAdminsThatShouldDeclare(Object... adminArgs) { } @Override - public synchronized void addArgument(String argName, Object argValue) { - this.arguments.put(argName, argValue); + public void addArgument(String argName, Object argValue) { + this.lock.lock(); + try { + this.arguments.put(argName, argValue); + } + finally { + this.lock.unlock(); + } } @Override - public synchronized Object removeArgument(String name) { - return this.arguments.remove(name); + public Object removeArgument(String name) { + this.lock.lock(); + try { + return this.arguments.remove(name); + } + finally { + this.lock.unlock(); + } } public Map getArguments() { diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java index 3f55623d1c..61775064dc 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java @@ -19,6 +19,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.aopalliance.aop.Advice; import org.apache.commons.logging.LogFactory; @@ -53,6 +55,7 @@ * A listener container for RabbitMQ Streams. * * @author Gary Russell + * @author Christian Tzolov * @since 2.4 * */ @@ -60,6 +63,8 @@ public class StreamListenerContainer extends ObservableListenerContainer { protected LogAccessor logger = new LogAccessor(LogFactory.getLog(getClass())); // NOSONAR + private final Lock lock = new ReentrantLock(); + private final ConsumerBuilder builder; private final Collection consumers = new ArrayList<>(); @@ -111,12 +116,18 @@ public StreamListenerContainer(Environment environment, @Nullable Codec codec) { * Mutually exclusive with {@link #superStream(String, String)}. */ @Override - public synchronized void setQueueNames(String... queueNames) { + public void setQueueNames(String... queueNames) { Assert.isTrue(!this.superStream, "setQueueNames() and superStream() are mutually exclusive"); Assert.isTrue(queueNames != null && queueNames.length == 1, "Only one stream is supported"); - this.builder.stream(queueNames[0]); - this.simpleStream = true; - this.streamName = queueNames[0]; + this.lock.lock(); + try { + this.builder.stream(queueNames[0]); + this.simpleStream = true; + this.streamName = queueNames[0]; + } + finally { + this.lock.unlock(); + } } /** @@ -139,16 +150,22 @@ public void superStream(String streamName, String name) { * @param consumers the number of consumers. * @since 3.0 */ - public synchronized void superStream(String streamName, String name, int consumers) { - Assert.isTrue(consumers > 0, () -> "'concurrency' must be greater than zero, not " + consumers); - this.concurrency = consumers; - Assert.isTrue(!this.simpleStream, "setQueueNames() and superStream() are mutually exclusive"); - Assert.notNull(streamName, "'superStream' cannot be null"); - this.builder.superStream(streamName) - .singleActiveConsumer() - .name(name); - this.superStream = true; - this.streamName = streamName; + public void superStream(String streamName, String name, int consumers) { + this.lock.lock(); + try { + Assert.isTrue(consumers > 0, () -> "'concurrency' must be greater than zero, not " + consumers); + this.concurrency = consumers; + Assert.isTrue(!this.simpleStream, "setQueueNames() and superStream() are mutually exclusive"); + Assert.notNull(streamName, "'superStream' cannot be null"); + this.builder.superStream(streamName) + .singleActiveConsumer() + .name(name); + this.superStream = true; + this.streamName = streamName; + } + finally { + this.lock.unlock(); + } } /** @@ -176,9 +193,15 @@ public void setStreamConverter(StreamMessageConverter messageConverter) { * Customize the consumer builder before it is built. * @param consumerCustomizer the customizer. */ - public synchronized void setConsumerCustomizer(ConsumerCustomizer consumerCustomizer) { - Assert.notNull(consumerCustomizer, "'consumerCustomizer' cannot be null"); - this.consumerCustomizer = consumerCustomizer; + public void setConsumerCustomizer(ConsumerCustomizer consumerCustomizer) { + this.lock.lock(); + try { + Assert.notNull(consumerCustomizer, "'consumerCustomizer' cannot be null"); + this.consumerCustomizer = consumerCustomizer; + } + finally { + this.lock.unlock(); + } } @Override @@ -225,36 +248,54 @@ public void afterPropertiesSet() { } @Override - public synchronized boolean isRunning() { - return this.consumers.size() > 0; + public boolean isRunning() { + this.lock.lock(); + try { + return this.consumers.size() > 0; + } + finally { + this.lock.unlock(); + } } @Override - public synchronized void start() { - if (this.consumers.size() == 0) { - this.consumerCustomizer.accept(getListenerId(), this.builder); - if (this.simpleStream) { - this.consumers.add(this.builder.build()); - } - else { - for (int i = 0; i < this.concurrency; i++) { + public void start() { + this.lock.lock(); + try { + if (this.consumers.size() == 0) { + this.consumerCustomizer.accept(getListenerId(), this.builder); + if (this.simpleStream) { this.consumers.add(this.builder.build()); } + else { + for (int i = 0; i < this.concurrency; i++) { + this.consumers.add(this.builder.build()); + } + } } } + finally { + this.lock.unlock(); + } } @Override - public synchronized void stop() { - this.consumers.forEach(consumer -> { - try { - consumer.close(); - } - catch (RuntimeException ex) { - this.logger.error(ex, "Failed to close consumer"); - } - }); - this.consumers.clear(); + public void stop() { + this.lock.lock(); + try { + this.consumers.forEach(consumer -> { + try { + consumer.close(); + } + catch (RuntimeException ex) { + this.logger.error(ex, "Failed to close consumer"); + } + }); + this.consumers.clear(); + } + finally { + this.lock.unlock(); + } } @Override diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java index 5a8c635748..0751e7dbd3 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java @@ -17,6 +17,8 @@ package org.springframework.rabbit.stream.producer; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.function.Function; import org.springframework.amqp.core.Message; @@ -52,6 +54,7 @@ * Default implementation of {@link RabbitStreamOperations}. * * @author Gary Russell + * @author Christian Tzolov * @since 2.4 * */ @@ -59,6 +62,8 @@ public class RabbitStreamTemplate implements RabbitStreamOperations, Application protected final LogAccessor logger = new LogAccessor(getClass()); // NOSONAR + private final Lock lock = new ReentrantLock(); + private ApplicationContext applicationContext; private final Environment environment; @@ -101,24 +106,30 @@ public RabbitStreamTemplate(Environment environment, String streamName) { } - private synchronized Producer createOrGetProducer() { - if (this.producer == null) { - ProducerBuilder builder = this.environment.producerBuilder(); - if (this.superStreamRouting == null) { - builder.stream(this.streamName); - } - else { - builder.superStream(this.streamName) - .routing(this.superStreamRouting); - } - this.producerCustomizer.accept(this.beanName, builder); - this.producer = builder.build(); - if (!this.streamConverterSet) { - ((DefaultStreamMessageConverter) this.streamConverter).setBuilderSupplier( - () -> this.producer.messageBuilder()); + private Producer createOrGetProducer() { + this.lock.lock(); + try { + if (this.producer == null) { + ProducerBuilder builder = this.environment.producerBuilder(); + if (this.superStreamRouting == null) { + builder.stream(this.streamName); + } + else { + builder.superStream(this.streamName) + .routing(this.superStreamRouting); + } + this.producerCustomizer.accept(this.beanName, builder); + this.producer = builder.build(); + if (!this.streamConverterSet) { + ((DefaultStreamMessageConverter) this.streamConverter).setBuilderSupplier( + () -> this.producer.messageBuilder()); + } } + return this.producer; + } + finally { + this.lock.unlock(); } - return this.producer; } @Override @@ -127,8 +138,14 @@ public void setApplicationContext(ApplicationContext applicationContext) throws } @Override - public synchronized void setBeanName(String name) { - this.beanName = name; + public void setBeanName(String name) { + this.lock.lock(); + try { + this.beanName = name; + } + finally { + this.lock.unlock(); + } } /** @@ -136,8 +153,14 @@ public synchronized void setBeanName(String name) { * @param superStreamRouting the routing function. * @since 3.0 */ - public synchronized void setSuperStreamRouting(Function superStreamRouting) { - this.superStreamRouting = superStreamRouting; + public void setSuperStreamRouting(Function superStreamRouting) { + this.lock.lock(); + try { + this.superStreamRouting = superStreamRouting; + } + finally { + this.lock.unlock(); + } } @@ -155,19 +178,31 @@ public void setMessageConverter(MessageConverter messageConverter) { * for {@link #send(Message)} and {@link #convertAndSend(Object)} methods. * @param streamConverter the converter. */ - public synchronized void setStreamConverter(StreamMessageConverter streamConverter) { + public void setStreamConverter(StreamMessageConverter streamConverter) { Assert.notNull(streamConverter, "'streamConverter' cannot be null"); - this.streamConverter = streamConverter; - this.streamConverterSet = true; + this.lock.lock(); + try { + this.streamConverter = streamConverter; + this.streamConverterSet = true; + } + finally { + this.lock.unlock(); + } } /** * Used to customize the {@link ProducerBuilder} before the {@link Producer} is built. * @param producerCustomizer the customizer; */ - public synchronized void setProducerCustomizer(ProducerCustomizer producerCustomizer) { + public void setProducerCustomizer(ProducerCustomizer producerCustomizer) { Assert.notNull(producerCustomizer, "'producerCustomizer' cannot be null"); - this.producerCustomizer = producerCustomizer; + this.lock.lock(); + try { + this.producerCustomizer = producerCustomizer; + } + finally { + this.lock.unlock(); + } } /** @@ -303,10 +338,16 @@ private ConfirmationHandler handleConfirm(CompletableFuture future, Obs * operation that requires one. */ @Override - public synchronized void close() { - if (this.producer != null) { - this.producer.close(); - this.producer = null; + public void close() { + this.lock.lock(); + try { + if (this.producer != null) { + this.producer.close(); + this.producer = null; + } + } + finally { + this.lock.unlock(); } } diff --git a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/TestRabbitTemplate.java b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/TestRabbitTemplate.java index 246cf0dbd3..c2c74b9405 100644 --- a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/TestRabbitTemplate.java +++ b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/TestRabbitTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 the original author or authors. + * Copyright 2017-2023 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. @@ -29,6 +29,8 @@ import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Stream; import org.springframework.amqp.core.Message; @@ -60,6 +62,7 @@ * * @author Gary Russell * @author Artem Bilan + * @author Christian Tzolov * * @since 2.0 * @@ -186,6 +189,8 @@ else if (listener instanceof MessageListener) { private static class Listeners { + private final Lock lock = new ReentrantLock(); + private final List listeners = new ArrayList<>(); private volatile Iterator iterator; @@ -193,11 +198,17 @@ private static class Listeners { Listeners() { } - private synchronized Object next() { - if (this.iterator == null || !this.iterator.hasNext()) { - this.iterator = this.listeners.iterator(); + private Object next() { + this.lock.lock(); + try { + if (this.iterator == null || !this.iterator.hasNext()) { + this.iterator = this.listeners.iterator(); + } + return this.iterator.next(); + } + finally { + this.lock.unlock(); } - return this.iterator.next(); } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java index f670255238..30c3f18908 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2023 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. @@ -22,6 +22,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -95,6 +97,8 @@ public class AsyncRabbitTemplate implements AsyncAmqpTemplate, ChannelAwareMessa private final Log logger = LogFactory.getLog(this.getClass()); + private final Lock lock = new ReentrantLock(); + private final RabbitTemplate template; private final AbstractMessageListenerContainer container; @@ -337,10 +341,16 @@ public void setReceiveTimeout(long receiveTimeout) { * @param taskScheduler the task scheduler * @see #setReceiveTimeout(long) */ - public synchronized void setTaskScheduler(TaskScheduler taskScheduler) { + public void setTaskScheduler(TaskScheduler taskScheduler) { Assert.notNull(taskScheduler, "'taskScheduler' cannot be null"); - this.internalTaskScheduler = false; - this.taskScheduler = taskScheduler; + this.lock.lock(); + try { + this.internalTaskScheduler = false; + this.taskScheduler = taskScheduler; + } + finally { + this.lock.unlock(); + } } /** @@ -510,44 +520,56 @@ private void sendDirect(Channel channel, String exchange, String routingKey, Mes } @Override - public synchronized void start() { - if (!this.running) { - if (this.internalTaskScheduler) { - ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); - scheduler.setThreadNamePrefix(getBeanName() == null ? "asyncTemplate-" : (getBeanName() + "-")); - scheduler.afterPropertiesSet(); - this.taskScheduler = scheduler; - } - if (this.container != null) { - this.container.start(); - } - if (this.directReplyToContainer != null) { - this.directReplyToContainer.setTaskScheduler(this.taskScheduler); - this.directReplyToContainer.start(); + public void start() { + this.lock.lock(); + try { + if (!this.running) { + if (this.internalTaskScheduler) { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setThreadNamePrefix(getBeanName() == null ? "asyncTemplate-" : (getBeanName() + "-")); + scheduler.afterPropertiesSet(); + this.taskScheduler = scheduler; + } + if (this.container != null) { + this.container.start(); + } + if (this.directReplyToContainer != null) { + this.directReplyToContainer.setTaskScheduler(this.taskScheduler); + this.directReplyToContainer.start(); + } } + this.running = true; + } + finally { + this.lock.unlock(); } - this.running = true; } @Override - public synchronized void stop() { - if (this.running) { - if (this.container != null) { - this.container.stop(); - } - if (this.directReplyToContainer != null) { - this.directReplyToContainer.stop(); - } - for (RabbitFuture future : this.pending.values()) { - future.setNackCause("AsyncRabbitTemplate was stopped while waiting for reply"); - future.cancel(true); - } - if (this.internalTaskScheduler) { - ((ThreadPoolTaskScheduler) this.taskScheduler).destroy(); - this.taskScheduler = null; + public void stop() { + this.lock.lock(); + try { + if (this.running) { + if (this.container != null) { + this.container.stop(); + } + if (this.directReplyToContainer != null) { + this.directReplyToContainer.stop(); + } + for (RabbitFuture future : this.pending.values()) { + future.setNackCause("AsyncRabbitTemplate was stopped while waiting for reply"); + future.cancel(true); + } + if (this.internalTaskScheduler) { + ((ThreadPoolTaskScheduler) this.taskScheduler).destroy(); + this.taskScheduler = null; + } } + this.running = false; + } + finally { + this.lock.unlock(); } - this.running = false; } @Override @@ -671,7 +693,8 @@ private void canceler(String correlationId, @Nullable ChannelHolder channelHolde @Nullable private ScheduledFuture timeoutTask(RabbitFuture future) { if (this.receiveTimeout > 0) { - synchronized (this) { + this.lock.lock(); + try { if (!this.running) { this.pending.remove(future.getCorrelationId()); throw new IllegalStateException("'AsyncRabbitTemplate' must be started."); @@ -680,6 +703,9 @@ private ScheduledFuture timeoutTask(RabbitFuture future) { new TimeoutTask(future, this.pending, this.directReplyToContainer), Instant.now().plusMillis(this.receiveTimeout)); } + finally { + this.lock.unlock(); + } } return null; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java index 312e7b20de..2acc7e2546 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java @@ -32,6 +32,8 @@ import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -68,6 +70,7 @@ * @author Steve Powell * @author Artem Bilan * @author Will Droste + * @author Christian Tzolov * */ public abstract class AbstractConnectionFactory implements ConnectionFactory, DisposableBean, BeanNameAware, @@ -80,14 +83,12 @@ public abstract class AbstractConnectionFactory implements ConnectionFactory, Di public enum AddressShuffleMode { /** - * Do not shuffle the addresses before or after opening a connection; attempt - * connections in a fixed order. + * Do not shuffle the addresses before or after opening a connection; attempt connections in a fixed order. */ NONE, /** - * Randomly shuffle the addresses before opening a connection; attempt connections - * in the new order. + * Randomly shuffle the addresses before opening a connection; attempt connections in the new order. */ RANDOM, @@ -106,6 +107,8 @@ public enum AddressShuffleMode { protected final Log logger = LogFactory.getLog(getClass()); // NOSONAR + private final Lock lock = new ReentrantLock(); + private final com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory; private final CompositeConnectionListener connectionListener = new CompositeConnectionListener(); @@ -144,10 +147,10 @@ public void handleRecovery(Recoverable recoverable) { private int closeTimeout = DEFAULT_CLOSE_TIMEOUT; - private ConnectionNameStrategy connectionNameStrategy = - connectionFactory -> (this.beanName != null ? this.beanName : "SpringAMQP") + - "#" + ObjectUtils.getIdentityHexString(this) + ":" + - this.defaultConnectionNameStrategyCounter.getAndIncrement(); + private ConnectionNameStrategy connectionNameStrategy = connectionFactory -> (this.beanName != null ? this.beanName + : "SpringAMQP") + + "#" + ObjectUtils.getIdentityHexString(this) + ":" + + this.defaultConnectionNameStrategyCounter.getAndIncrement(); private String beanName; @@ -160,8 +163,8 @@ public void handleRecovery(Recoverable recoverable) { private volatile boolean contextStopped; /** - * Create a new AbstractConnectionFactory for the given target ConnectionFactory, - * with no publisher connection factory. + * Create a new AbstractConnectionFactory for the given target ConnectionFactory, with no publisher connection + * factory. * @param rabbitConnectionFactory the target ConnectionFactory */ public AbstractConnectionFactory(com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory) { @@ -170,8 +173,7 @@ public AbstractConnectionFactory(com.rabbitmq.client.ConnectionFactory rabbitCon } /** - * Set a custom publisher connection factory; the type does not need to be the same - * as this factory. + * Set a custom publisher connection factory; the type does not need to be the same as this factory. * @param publisherConnectionFactory the factory. * @since 2.3.2 */ @@ -266,8 +268,8 @@ public void setConnectionThreadFactory(ThreadFactory threadFactory) { } /** - * Set an {@link AddressResolver} to use when creating connections; overrides - * {@link #setAddresses(String)}, {@link #setHost(String)}, and {@link #setPort(int)}. + * Set an {@link AddressResolver} to use when creating connections; overrides {@link #setAddresses(String)}, + * {@link #setHost(String)}, and {@link #setPort(int)}. * @param addressResolver the resolver. * @since 2.1.15 */ @@ -336,29 +338,40 @@ public int getPort() { } /** - * Set addresses for clustering. - * This property overrides the host+port properties if not empty. + * Set addresses for clustering. This property overrides the host+port properties if not empty. * @param addresses list of addresses with form "host[:port],..." */ - public synchronized void setAddresses(String addresses) { - if (StringUtils.hasText(addresses)) { - Address[] addressArray = Address.parseAddresses(addresses); - if (addressArray.length > 0) { - this.addresses = new LinkedList<>(Arrays.asList(addressArray)); - if (this.publisherConnectionFactory != null) { - this.publisherConnectionFactory.setAddresses(addresses); + public void setAddresses(String addresses) { + this.lock.lock(); + try { + if (StringUtils.hasText(addresses)) { + Address[] addressArray = Address.parseAddresses(addresses); + if (addressArray.length > 0) { + this.addresses = new LinkedList<>(Arrays.asList(addressArray)); + if (this.publisherConnectionFactory != null) { + this.publisherConnectionFactory.setAddresses(addresses); + } + return; } - return; } + this.logger.info("setAddresses() called with an empty value, will be using the host+port " + + " or addressResolver properties for connections"); + this.addresses = null; + } + finally { + this.lock.unlock(); } - this.logger.info("setAddresses() called with an empty value, will be using the host+port " - + " or addressResolver properties for connections"); - this.addresses = null; } @Nullable - protected synchronized List
getAddresses() throws IOException { - return this.addressResolver != null ? this.addressResolver.getAddresses() : this.addresses; + protected List
getAddresses() throws IOException { + this.lock.lock(); + try { + return this.addressResolver != null ? this.addressResolver.getAddresses() : this.addresses; + } + finally { + this.lock.unlock(); + } } /** @@ -435,10 +448,8 @@ public void addChannelListener(ChannelListener listener) { } /** - * Provide an Executor for - * use by the Rabbit ConnectionFactory when creating connections. - * Can either be an ExecutorService or a Spring - * ThreadPoolTaskExecutor, as defined by a <task:executor/> element. + * Provide an Executor for use by the Rabbit ConnectionFactory when creating connections. Can either be an + * ExecutorService or a Spring ThreadPoolTaskExecutor, as defined by a <task:executor/> element. * @param executor The executor. */ public void setExecutor(Executor executor) { @@ -463,8 +474,8 @@ protected ExecutorService getExecutorService() { } /** - * How long to wait (milliseconds) for a response to a connection close - * operation from the broker; default 30000 (30 seconds). + * How long to wait (milliseconds) for a response to a connection close operation from the broker; default 30000 (30 + * seconds). * @param closeTimeout the closeTimeout to set. */ public void setCloseTimeout(int closeTimeout) { @@ -479,8 +490,8 @@ public int getCloseTimeout() { } /** - * Provide a {@link ConnectionNameStrategy} to build the name for the target RabbitMQ connection. - * The {@link #beanName} together with a counter is used by default. + * Provide a {@link ConnectionNameStrategy} to build the name for the target RabbitMQ connection. The + * {@link #beanName} together with a counter is used by default. * @param connectionNameStrategy the {@link ConnectionNameStrategy} to use. * @since 2.0 */ @@ -493,10 +504,10 @@ public void setConnectionNameStrategy(ConnectionNameStrategy connectionNameStrat } /** - * Set the strategy for logging close exceptions; by default, if a channel is closed due to a failed - * passive queue declaration, it is logged at debug level. Normal channel closes (200 OK) are not - * logged. All others are logged at ERROR level (unless access is refused due to an exclusive consumer - * condition, in which case, it is logged at DEBUG level, since 3.1, previously INFO). + * Set the strategy for logging close exceptions; by default, if a channel is closed due to a failed passive queue + * declaration, it is logged at debug level. Normal channel closes (200 OK) are not logged. All others are logged at + * ERROR level (unless access is refused due to an exclusive consumer condition, in which case, it is logged at + * DEBUG level, since 3.1, previously INFO). * @param closeExceptionLogger the {@link ConditionalExceptionLogger}. * @since 1.5 */ @@ -585,7 +596,8 @@ public void handleRecovery(Recoverable recoverable) { } if (this.applicationEventPublisher != null) { - connection.addBlockedListener(new ConnectionBlockedListener(connection, this.applicationEventPublisher)); + connection + .addBlockedListener(new ConnectionBlockedListener(connection, this.applicationEventPublisher)); } return connection; @@ -597,17 +609,23 @@ public void handleRecovery(Recoverable recoverable) { } } - private synchronized com.rabbitmq.client.Connection connect(String connectionName) + private com.rabbitmq.client.Connection connect(String connectionName) throws IOException, TimeoutException { - if (this.addressResolver != null) { - return connectResolver(connectionName); - } - if (this.addresses != null) { - return connectAddresses(connectionName); + this.lock.lock(); + try { + if (this.addressResolver != null) { + return connectResolver(connectionName); + } + if (this.addresses != null) { + return connectAddresses(connectionName); + } + else { + return connectHostPort(connectionName); + } } - else { - return connectHostPort(connectionName); + finally { + this.lock.unlock(); } } @@ -619,22 +637,28 @@ private com.rabbitmq.client.Connection connectResolver(String connectionName) th connectionName); } - private synchronized com.rabbitmq.client.Connection connectAddresses(String connectionName) + private com.rabbitmq.client.Connection connectAddresses(String connectionName) throws IOException, TimeoutException { - List
addressesToConnect = new ArrayList<>(this.addresses); - if (addressesToConnect.size() > 1 && AddressShuffleMode.RANDOM.equals(this.addressShuffleMode)) { - Collections.shuffle(addressesToConnect); - } - if (this.logger.isInfoEnabled()) { - this.logger.info("Attempting to connect to: " + addressesToConnect); + this.lock.lock(); + try { + List
addressesToConnect = new ArrayList<>(this.addresses); + if (addressesToConnect.size() > 1 && AddressShuffleMode.RANDOM.equals(this.addressShuffleMode)) { + Collections.shuffle(addressesToConnect); + } + if (this.logger.isInfoEnabled()) { + this.logger.info("Attempting to connect to: " + addressesToConnect); + } + com.rabbitmq.client.Connection connection = this.rabbitConnectionFactory.newConnection(this.executorService, + addressesToConnect, connectionName); + if (addressesToConnect.size() > 1 && AddressShuffleMode.INORDER.equals(this.addressShuffleMode)) { + this.addresses.add(this.addresses.remove(0)); + } + return connection; } - com.rabbitmq.client.Connection connection = this.rabbitConnectionFactory.newConnection(this.executorService, - addressesToConnect, connectionName); - if (addressesToConnect.size() > 1 && AddressShuffleMode.INORDER.equals(this.addressShuffleMode)) { - this.addresses.add(this.addresses.remove(0)); + finally { + this.lock.unlock(); } - return connection; } private com.rabbitmq.client.Connection connectHostPort(String connectionName) throws IOException, TimeoutException { @@ -716,8 +740,7 @@ public void handleUnblocked() { } /** - * Default implementation of {@link ConditionalExceptionLogger} for logging channel - * close exceptions. + * Default implementation of {@link ConditionalExceptionLogger} for logging channel close exceptions. * @since 1.5 */ public static class DefaultChannelCloseLogger implements ConditionalExceptionLogger { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java index e83020bec0..e84ed71190 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java @@ -21,6 +21,8 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -49,6 +51,7 @@ *

All {@link ConnectionFactory} methods delegate to the default * * @author Gary Russell + * @author Christian Tzolov * @since 1.2 */ public class LocalizedQueueConnectionFactory implements ConnectionFactory, RoutingConnectionFactory, DisposableBean, @@ -56,6 +59,8 @@ public class LocalizedQueueConnectionFactory implements ConnectionFactory, Routi private final Log logger = LogFactory.getLog(getClass()); + private final Lock lock = new ReentrantLock(); + private final Map nodeFactories = new HashMap(); private final ConnectionFactory defaultConnectionFactory; @@ -299,19 +304,25 @@ private ConnectionFactory determineConnectionFactory(String queue) { return cf; } - private synchronized ConnectionFactory nodeConnectionFactory(String queue, String node, String address) { - if (this.logger.isInfoEnabled()) { - this.logger.info("Queue: " + queue + " is on node: " + node + " at: " + address); - } - ConnectionFactory cf = this.nodeFactories.get(node); - if (cf == null) { - cf = createConnectionFactory(address, node); + private ConnectionFactory nodeConnectionFactory(String queue, String node, String address) { + this.lock.lock(); + try { if (this.logger.isInfoEnabled()) { - this.logger.info("Created new connection factory: " + cf); + this.logger.info("Queue: " + queue + " is on node: " + node + " at: " + address); } - this.nodeFactories.put(node, cf); + ConnectionFactory cf = this.nodeFactories.get(node); + if (cf == null) { + cf = createConnectionFactory(address, node); + if (this.logger.isInfoEnabled()) { + this.logger.info("Created new connection factory: " + cf); + } + this.nodeFactories.put(node, cf); + } + return cf; + } + finally { + this.lock.unlock(); } - return cf; } /** diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java index a1092f1f7b..b2072f31f9 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java @@ -20,6 +20,8 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.function.BiConsumer; import org.aopalliance.aop.Advice; @@ -52,6 +54,7 @@ * * @author Gary Russell * @author Leonardo Ferreira + * @author Christian Tzolov * @since 2.3 * */ @@ -60,6 +63,8 @@ public class PooledChannelConnectionFactory extends AbstractConnectionFactory private final AtomicBoolean running = new AtomicBoolean(); + private final Lock lock = new ReentrantLock(); + private volatile ConnectionWrapper connection; private boolean simplePublisherConfirms; @@ -158,14 +163,20 @@ public boolean isRunning() { } @Override - public synchronized Connection createConnection() throws AmqpException { - if (this.connection == null || !this.connection.isOpen()) { - Connection bareConnection = createBareConnection(); // NOSONAR - see destroy() - this.connection = new ConnectionWrapper(bareConnection.getDelegate(), getCloseTimeout(), // NOSONAR - this.simplePublisherConfirms, this.poolConfigurer, getChannelListener()); // NOSONAR - getConnectionListener().onCreate(this.connection); + public Connection createConnection() throws AmqpException { + this.lock.lock(); + try { + if (this.connection == null || !this.connection.isOpen()) { + Connection bareConnection = createBareConnection(); // NOSONAR - see destroy() + this.connection = new ConnectionWrapper(bareConnection.getDelegate(), getCloseTimeout(), // NOSONAR + this.simplePublisherConfirms, this.poolConfigurer, getChannelListener()); // NOSONAR + getConnectionListener().onCreate(this.connection); + } + return this.connection; + } + finally { + this.lock.unlock(); } - return this.connection; } /** @@ -180,12 +191,18 @@ public void resetConnection() { } @Override - public synchronized void destroy() { - super.destroy(); - if (this.connection != null) { - this.connection.forceClose(); - getConnectionListener().onClose(this.connection); - this.connection = null; + public void destroy() { + this.lock.lock(); + try { + super.destroy(); + if (this.connection != null) { + this.connection.forceClose(); + getConnectionListener().onClose(this.connection); + this.connection = null; + } + } + finally { + this.lock.unlock(); } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java index 70ba93125c..b0242d86c0 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -34,6 +34,8 @@ import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeoutException; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -88,6 +90,7 @@ * @author Gary Russell * @author Arnaud Cogoluègnes * @author Artem Bilan + * @author Christian Tzolov * * @since 1.0.1 * @@ -99,6 +102,8 @@ public class PublisherCallbackChannelImpl private final Log logger = LogFactory.getLog(this.getClass()); + private final Lock lock = new ReentrantLock(); + private final Channel delegate; private final ConcurrentMap listeners = new ConcurrentHashMap<>(); @@ -127,16 +132,21 @@ public PublisherCallbackChannelImpl(Channel delegate, ExecutorService executor) } @Override - public synchronized void setAfterAckCallback(java.util.function.Consumer callback) { - if (getPendingConfirmsCount() == 0 && callback != null) { - callback.accept(this); + public void setAfterAckCallback(java.util.function.Consumer callback) { + this.lock.lock(); + try { + if (getPendingConfirmsCount() == 0 && callback != null) { + callback.accept(this); + } + else { + this.afterAckCallback = callback; + } } - else { - this.afterAckCallback = callback; + finally { + this.lock.unlock(); } } - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // BEGIN PURE DELEGATE METHODS ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -721,8 +731,14 @@ public boolean removeReturnListener(ReturnListener listener) { } @Override - public synchronized void clearReturnListeners() { - this.delegate.clearReturnListeners(); + public void clearReturnListeners() { + this.lock.lock(); + try { + this.delegate.clearReturnListeners(); + } + finally { + this.lock.unlock(); + } } @Override @@ -824,42 +840,60 @@ private void shutdownCompleted(String cause) { this.executor.execute(() -> generateNacksForPendingAcks(cause)); } - private synchronized void generateNacksForPendingAcks(String cause) { - for (Entry> entry : this.pendingConfirms.entrySet()) { - Listener listener = entry.getKey(); - for (Entry confirmEntry : entry.getValue().entrySet()) { - confirmEntry.getValue().setCause(cause); - if (this.logger.isDebugEnabled()) { - this.logger.debug(this.toString() + " PC:Nack:(close):" + confirmEntry.getKey()); + private void generateNacksForPendingAcks(String cause) { + this.lock.lock(); + try { + for (Entry> entry : this.pendingConfirms.entrySet()) { + Listener listener = entry.getKey(); + for (Entry confirmEntry : entry.getValue().entrySet()) { + confirmEntry.getValue().setCause(cause); + if (this.logger.isDebugEnabled()) { + this.logger.debug(this.toString() + " PC:Nack:(close):" + confirmEntry.getKey()); + } + processAck(confirmEntry.getKey(), false, false, false); } - processAck(confirmEntry.getKey(), false, false, false); + listener.revoke(this); + } + if (this.logger.isDebugEnabled()) { + this.logger.debug("PendingConfirms cleared"); } - listener.revoke(this); + this.pendingConfirms.clear(); + this.listenerForSeq.clear(); + this.listeners.clear(); } - if (this.logger.isDebugEnabled()) { - this.logger.debug("PendingConfirms cleared"); + finally { + this.lock.unlock(); } - this.pendingConfirms.clear(); - this.listenerForSeq.clear(); - this.listeners.clear(); } @Override - public synchronized int getPendingConfirmsCount(Listener listener) { - SortedMap pendingConfirmsForListener = this.pendingConfirms.get(listener); - if (pendingConfirmsForListener == null) { - return 0; + public int getPendingConfirmsCount(Listener listener) { + this.lock.lock(); + try { + SortedMap pendingConfirmsForListener = this.pendingConfirms.get(listener); + if (pendingConfirmsForListener == null) { + return 0; + } + else { + return pendingConfirmsForListener.entrySet().size(); + } } - else { - return pendingConfirmsForListener.entrySet().size(); + finally { + this.lock.unlock(); } } @Override - public synchronized int getPendingConfirmsCount() { - return this.pendingConfirms.values().stream() - .mapToInt(Map::size) - .sum(); + public int getPendingConfirmsCount() { + this.lock.lock(); + try { + return this.pendingConfirms.values().stream() + .mapToInt(Map::size) + .sum(); + } + finally { + this.lock.unlock(); + } } /** @@ -882,29 +916,35 @@ public void addListener(Listener listener) { } @Override - public synchronized Collection expire(Listener listener, long cutoffTime) { - SortedMap pendingConfirmsForListener = this.pendingConfirms.get(listener); - if (pendingConfirmsForListener == null) { - return Collections.emptyList(); - } - else { - List expired = new ArrayList(); - Iterator> iterator = pendingConfirmsForListener.entrySet().iterator(); - while (iterator.hasNext()) { - PendingConfirm pendingConfirm = iterator.next().getValue(); - if (pendingConfirm.getTimestamp() < cutoffTime) { - expired.add(pendingConfirm); - iterator.remove(); - CorrelationData correlationData = pendingConfirm.getCorrelationData(); - if (correlationData != null && StringUtils.hasText(correlationData.getId())) { - this.pendingReturns.remove(correlationData.getId()); // NOSONAR never null + public Collection expire(Listener listener, long cutoffTime) { + this.lock.lock(); + try { + SortedMap pendingConfirmsForListener = this.pendingConfirms.get(listener); + if (pendingConfirmsForListener == null) { + return Collections.emptyList(); + } + else { + List expired = new ArrayList(); + Iterator> iterator = pendingConfirmsForListener.entrySet().iterator(); + while (iterator.hasNext()) { + PendingConfirm pendingConfirm = iterator.next().getValue(); + if (pendingConfirm.getTimestamp() < cutoffTime) { + expired.add(pendingConfirm); + iterator.remove(); + CorrelationData correlationData = pendingConfirm.getCorrelationData(); + if (correlationData != null && StringUtils.hasText(correlationData.getId())) { + this.pendingReturns.remove(correlationData.getId()); // NOSONAR never null + } + } + else { + break; } } - else { - break; - } + return expired; } - return expired; + } + finally { + this.lock.unlock(); } } @@ -926,12 +966,18 @@ public void handleNack(long seq, boolean multiple) { processAck(seq, false, multiple, true); } - private synchronized void processAck(long seq, boolean ack, boolean multiple, boolean remove) { + private void processAck(long seq, boolean ack, boolean multiple, boolean remove) { + this.lock.lock(); try { - doProcessAck(seq, ack, multiple, remove); + try { + doProcessAck(seq, ack, multiple, remove); + } + catch (Exception e) { + this.logger.error("Failed to process publisher confirm", e); + } } - catch (Exception e) { - this.logger.error("Failed to process publisher confirm", e); + finally { + this.lock.unlock(); } } @@ -1028,12 +1074,18 @@ private void doHandleConfirm(boolean ack, Listener listener, PendingConfirm pend try { if (this.afterAckCallback != null) { java.util.function.Consumer callback = null; - synchronized (this) { + this.lock.lock(); + try { if (getPendingConfirmsCount() == 0) { callback = this.afterAckCallback; this.afterAckCallback = null; } + + } + finally { + this.lock.unlock(); } + if (callback != null) { callback.accept(this); } @@ -1048,18 +1100,24 @@ private void doHandleConfirm(boolean ack, Listener listener, PendingConfirm pend } @Override - public synchronized void addPendingConfirm(Listener listener, long seq, PendingConfirm pendingConfirm) { - SortedMap pendingConfirmsForListener = this.pendingConfirms.get(listener); - Assert.notNull(pendingConfirmsForListener, - "Listener not registered: " + listener + " " + this.pendingConfirms.keySet()); - pendingConfirmsForListener.put(seq, pendingConfirm); - this.listenerForSeq.put(seq, listener); - if (pendingConfirm.getCorrelationData() != null) { - String returnCorrelation = pendingConfirm.getCorrelationData().getId(); // NOSONAR never null - if (StringUtils.hasText(returnCorrelation)) { - this.pendingReturns.put(returnCorrelation, pendingConfirm); + public void addPendingConfirm(Listener listener, long seq, PendingConfirm pendingConfirm) { + this.lock.lock(); + try { + SortedMap pendingConfirmsForListener = this.pendingConfirms.get(listener); + Assert.notNull(pendingConfirmsForListener, + "Listener not registered: " + listener + " " + this.pendingConfirms.keySet()); + pendingConfirmsForListener.put(seq, pendingConfirm); + this.listenerForSeq.put(seq, listener); + if (pendingConfirm.getCorrelationData() != null) { + String returnCorrelation = pendingConfirm.getCorrelationData().getId(); // NOSONAR never null + if (StringUtils.hasText(returnCorrelation)) { + this.pendingReturns.put(returnCorrelation, pendingConfirm); + } } } + finally { + this.lock.unlock(); + } } // ReturnListener diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java index d31d4413e4..d1b667532f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java @@ -22,6 +22,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import org.aopalliance.aop.Advice; @@ -46,12 +48,15 @@ * * @author Gary Russell * @author Leonardo Ferreira + * @author Christian Tzolov * @since 2.3 * */ public class ThreadChannelConnectionFactory extends AbstractConnectionFactory implements ShutdownListener, SmartLifecycle { + private final Lock lock = new ReentrantLock(); + private final Map contextSwitches = new ConcurrentHashMap<>(); private final Map switchesInProgress = new ConcurrentHashMap<>(); @@ -141,13 +146,19 @@ public void addConnectionListener(ConnectionListener listener) { } @Override - public synchronized Connection createConnection() throws AmqpException { - if (this.connection == null || !this.connection.isOpen()) { - Connection bareConnection = createBareConnection(); // NOSONAR - see destroy() - this.connection = new ConnectionWrapper(bareConnection.getDelegate(), getCloseTimeout()); // NOSONAR - getConnectionListener().onCreate(this.connection); + public Connection createConnection() throws AmqpException { + this.lock.lock(); + try { + if (this.connection == null || !this.connection.isOpen()) { + Connection bareConnection = createBareConnection(); // NOSONAR - see destroy() + this.connection = new ConnectionWrapper(bareConnection.getDelegate(), getCloseTimeout()); // NOSONAR + getConnectionListener().onCreate(this.connection); + } + return this.connection; + } + finally { + this.lock.unlock(); } - return this.connection; } /** @@ -172,21 +183,27 @@ public void resetConnection() { } @Override - public synchronized void destroy() { - super.destroy(); - if (this.connection != null) { - this.connection.forceClose(); - this.connection = null; + public void destroy() { + this.lock.lock(); + try { + super.destroy(); + if (this.connection != null) { + this.connection.forceClose(); + this.connection = null; + } + if (this.switchesInProgress.size() > 0 && this.logger.isWarnEnabled()) { + this.logger.warn("Unclaimed context switches from threads:" + + this.switchesInProgress.values() + .stream() + .map(t -> t.getName()) + .collect(Collectors.toList())); + } + this.contextSwitches.clear(); + this.switchesInProgress.clear(); } - if (this.switchesInProgress.size() > 0 && this.logger.isWarnEnabled()) { - this.logger.warn("Unclaimed context switches from threads:" + - this.switchesInProgress.values() - .stream() - .map(t -> t.getName()) - .collect(Collectors.toList())); + finally { + this.lock.unlock(); } - this.contextSwitches.clear(); - this.switchesInProgress.clear(); } /** diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplate.java index 8316b3e375..8982d80986 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2023 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. @@ -18,6 +18,8 @@ import java.util.Date; import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.springframework.amqp.AmqpException; import org.springframework.amqp.core.Message; @@ -43,6 +45,8 @@ */ public class BatchingRabbitTemplate extends RabbitTemplate { + private final Lock lock = new ReentrantLock(); + private final BatchingStrategy batchingStrategy; private final TaskScheduler scheduler; @@ -75,28 +79,33 @@ public BatchingRabbitTemplate(ConnectionFactory connectionFactory, BatchingStrat } @Override - public synchronized void send(String exchange, String routingKey, Message message, + public void send(String exchange, String routingKey, Message message, @Nullable CorrelationData correlationData) throws AmqpException { - - if (correlationData != null) { - if (this.logger.isDebugEnabled()) { - this.logger.debug("Cannot use batching with correlation data"); - } - super.send(exchange, routingKey, message, correlationData); - } - else { - if (this.scheduledTask != null) { - this.scheduledTask.cancel(false); - } - MessageBatch batch = this.batchingStrategy.addToBatch(exchange, routingKey, message); - if (batch != null) { - super.send(batch.getExchange(), batch.getRoutingKey(), batch.getMessage(), null); + this.lock.lock(); + try { + if (correlationData != null) { + if (this.logger.isDebugEnabled()) { + this.logger.debug("Cannot use batching with correlation data"); + } + super.send(exchange, routingKey, message, correlationData); } - Date next = this.batchingStrategy.nextRelease(); - if (next != null) { - this.scheduledTask = this.scheduler.schedule((Runnable) () -> releaseBatches(), next.toInstant()); + else { + if (this.scheduledTask != null) { + this.scheduledTask.cancel(false); + } + MessageBatch batch = this.batchingStrategy.addToBatch(exchange, routingKey, message); + if (batch != null) { + super.send(batch.getExchange(), batch.getRoutingKey(), batch.getMessage(), null); + } + Date next = this.batchingStrategy.nextRelease(); + if (next != null) { + this.scheduledTask = this.scheduler.schedule((Runnable) () -> releaseBatches(), next.toInstant()); + } } } + finally { + this.lock.unlock(); + } } /** @@ -106,9 +115,15 @@ public void flush() { releaseBatches(); } - private synchronized void releaseBatches() { - for (MessageBatch batch : this.batchingStrategy.releaseBatches()) { - super.send(batch.getExchange(), batch.getRoutingKey(), batch.getMessage(), null); + private void releaseBatches() { + this.lock.lock(); + try { + for (MessageBatch batch : this.batchingStrategy.releaseBatches()) { + super.send(batch.getExchange(), batch.getRoutingKey(), batch.getMessage(), null); + } + } + finally { + this.lock.unlock(); } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BrokerEventListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BrokerEventListener.java index 3b453b7e74..8c5113e87e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BrokerEventListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BrokerEventListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2023 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. @@ -17,6 +17,8 @@ package org.springframework.amqp.rabbit.core; import java.util.Arrays; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -50,6 +52,7 @@ * with the supplied keys. * * @author Gary Russell + * @author Christian Tzolov * @since 2.1 * */ @@ -58,6 +61,8 @@ public class BrokerEventListener implements MessageListener, ApplicationEventPub private static final Log logger = LogFactory.getLog(BrokerEventListener.class); // NOSONAR - lower case + private final Lock lock = new ReentrantLock(); + private final AbstractMessageListenerContainer container; private final String[] eventKeys; @@ -137,34 +142,52 @@ public void setApplicationEventPublisher(ApplicationEventPublisher applicationEv } @Override - public synchronized void start() { - if (!this.running) { - if (this.stopInvoked) { - // redeclare auto-delete queue - this.stopInvoked = false; - onCreate(null); - } - if (this.ownContainer) { - this.container.start(); + public void start() { + this.lock.lock(); + try { + if (!this.running) { + if (this.stopInvoked) { + // redeclare auto-delete queue + this.stopInvoked = false; + onCreate(null); + } + if (this.ownContainer) { + this.container.start(); + } + this.running = true; } - this.running = true; + } + finally { + this.lock.unlock(); } } @Override - public synchronized void stop() { - if (this.running) { - if (this.ownContainer) { - this.container.stop(); + public void stop() { + this.lock.lock(); + try { + if (this.running) { + if (this.ownContainer) { + this.container.stop(); + } + this.running = false; + this.stopInvoked = true; } - this.running = false; - this.stopInvoked = true; + } + finally { + this.lock.unlock(); } } @Override - public synchronized boolean isRunning() { - return this.running; + public boolean isRunning() { + this.lock.lock(); + try { + return this.running; + } + finally { + this.lock.unlock(); + } } @Override diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java index 5b957a426b..2eda92dc26 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java @@ -31,6 +31,8 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import org.apache.commons.logging.Log; @@ -79,6 +81,7 @@ * @author Ed Scriven * @author Gary Russell * @author Artem Bilan + * @author Christian Tzolov */ @ManagedResource(description = "Admin Tasks") public class RabbitAdmin implements AmqpAdmin, ApplicationContextAware, ApplicationEventPublisherAware, @@ -122,6 +125,8 @@ public class RabbitAdmin implements AmqpAdmin, ApplicationContextAware, Applicat /** Logger available to subclasses. */ protected final Log logger = LogFactory.getLog(getClass()); // NOSONAR + private final Lock lock = new ReentrantLock(); + private final RabbitTemplate rabbitTemplate; private final Object lifecycleMonitor = new Object(); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j2/AmqpAppender.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j2/AmqpAppender.java index de4888660f..d76c38a990 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j2/AmqpAppender.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j2/AmqpAppender.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2023 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. @@ -33,6 +33,8 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.core.Filter; @@ -139,7 +141,7 @@ public class AmqpAppender extends AbstractAppender { /** * Used to synchronize access to pattern layouts. */ - private final Object layoutMutex = new Object(); + private final Lock layoutMutex = new ReentrantLock(); /** * Construct an instance with the provided properties. @@ -256,10 +258,14 @@ protected void doSend(Event event, LogEvent logEvent, MessageProperties amqpProp StringBuilder msgBody; String routingKey; try { - synchronized (this.layoutMutex) { + this.layoutMutex.lock(); + try { msgBody = new StringBuilder(new String(getLayout().toByteArray(logEvent), StandardCharsets.UTF_8)); routingKey = new String(this.manager.routingKeyLayout.toByteArray(logEvent), StandardCharsets.UTF_8); } + finally { + this.layoutMutex.unlock(); + } Message message = null; if (this.manager.charset != null) { try { diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/RoutingConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/RoutingConnectionFactoryTests.java index c22f4b6e7f..ff5918bcec 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/RoutingConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/RoutingConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -37,6 +37,8 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -53,6 +55,7 @@ * @author Josh Chappelle * @author Gary Russell * @author Leonardo Ferreira + * @author Christian Tzolov * @since 1.3 */ public class RoutingConnectionFactoryTests { @@ -224,11 +227,18 @@ public void testWithSMLCAndConnectionListener() throws Exception { final AtomicReference connectionMakerKey2 = new AtomicReference<>(); SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory) { + Lock lock = new ReentrantLock(); + @Override - protected synchronized void redeclareElementsIfNecessary() { - connectionMakerKey2.set(connectionFactory.determineCurrentLookupKey()); + protected void redeclareElementsIfNecessary() { + this.lock.lock(); + try { + connectionMakerKey2.set(connectionFactory.determineCurrentLookupKey()); + } + finally { + this.lock.unlock(); + } } - }; container.setQueueNames("foo"); container.setLookupKeyQualifier("xxx"); @@ -263,9 +273,17 @@ public void testWithDMLCAndConnectionListener() throws Exception { final AtomicReference connectionMakerKey2 = new AtomicReference<>(); DirectMessageListenerContainer container = new DirectMessageListenerContainer(connectionFactory) { + Lock lock = new ReentrantLock(); + @Override - protected synchronized void redeclareElementsIfNecessary() { - connectionMakerKey2.set(connectionFactory.determineCurrentLookupKey()); + protected void redeclareElementsIfNecessary() { + this.lock.lock(); + try { + connectionMakerKey2.set(connectionFactory.determineCurrentLookupKey()); + } + finally { + this.lock.unlock(); + } } }; @@ -306,9 +324,17 @@ public void testWithDRTDMLCAndConnectionListenerExistingRFK() throws Exception { final AtomicReference connectionMakerKey2 = new AtomicReference<>(); DirectReplyToMessageListenerContainer container = new DirectReplyToMessageListenerContainer(connectionFactory) { + Lock lock = new ReentrantLock(); + @Override - protected synchronized void redeclareElementsIfNecessary() { - connectionMakerKey2.set(connectionFactory.determineCurrentLookupKey()); + protected void redeclareElementsIfNecessary() { + this.lock.lock(); + try { + connectionMakerKey2.set(connectionFactory.determineCurrentLookupKey()); + } + finally { + this.lock.unlock(); + } } }; From 0c4efb607d87a7fe3754ac4f6d04e789c6377f5a Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 13 Dec 2023 17:56:43 -0500 Subject: [PATCH 322/737] GH-2550: Fix CachingCF leak after reconnection Fixes: #2550 When a `CachingConnectionFactory` is used in a `SimpleMessageContainer` and then there is a connection reset (network glitch, Rabbit restart etc.,), the `SimpleMessageContainer` tries to restart the consumer. However` the checkout permits assigned to the lost channels in `CachingConnectionFactory` is not reclaimed. So the consumer is unable to create channels and recover. * Fix `BlockingQueueConsumer.forceCloseAndClearQueue()` to not check `channel.isOpen()` and always perform respective cleanups, including releasing permits for channels in the `CachingConnectionFactory`. If channel is closed for the network reset reason, it is going to be recreated in the cache. * Add `ConsumerConnectionRecoveryTests` to verify that consumer is really consuming after reconnection. Use Testcontainers to restart `RabbitMQContainer` in the middle of the test **Cherry-pick to `3.0.x`** --- .../listener/BlockingQueueConsumer.java | 2 +- .../ConsumerConnectionRecoveryTests.java | 127 ++++++++++++++++++ 2 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConsumerConnectionRecoveryTests.java diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java index 757e3b63e4..c6a95816af 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java @@ -788,7 +788,7 @@ public synchronized void stop() { } public void forceCloseAndClearQueue() { - if (this.channel != null && this.channel.isOpen()) { + if (this.channel != null) { RabbitUtils.setPhysicalCloseRequired(this.channel, true); ConnectionFactoryUtils.releaseResources(this.resourceHolder); this.deliveryTags.clear(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConsumerConnectionRecoveryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConsumerConnectionRecoveryTests.java new file mode 100644 index 0000000000..1ddf92fa74 --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConsumerConnectionRecoveryTests.java @@ -0,0 +1,127 @@ +/* + * Copyright 2023 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.connection; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.RabbitMQContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import org.springframework.amqp.rabbit.annotation.EnableRabbit; +import org.springframework.amqp.rabbit.annotation.Queue; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + + +/** + * @author Artem Bilan + * + * @since 3.0.11 + * + */ +@SpringJUnitConfig +@Testcontainers(disabledWithoutDocker = true) +@DirtiesContext +public class ConsumerConnectionRecoveryTests { + + @Container + static final RabbitMQContainer RABBIT_MQ_CONTAINER = + new RabbitMQContainer(DockerImageName.parse("rabbitmq")); + + @Test + void verifyThatChannelPermitsAreReleaseOnReconnect(@Autowired TestConfiguration application) + throws InterruptedException { + + application.rabbitTemplate().convertAndSend("testQueue", "test data #1"); + + assertThat(application.received.poll(20, TimeUnit.SECONDS)).isEqualTo("test data #1"); + + RABBIT_MQ_CONTAINER.stop(); + RABBIT_MQ_CONTAINER.start(); + + application.connectionFactory().setPort(RABBIT_MQ_CONTAINER.getAmqpPort()); + application.publisherConnectionFactory().setPort(RABBIT_MQ_CONTAINER.getAmqpPort()); + + application.rabbitTemplate().convertAndSend("testQueue", "test data #2"); + + assertThat(application.received.poll(30, TimeUnit.SECONDS)).isEqualTo("test data #2"); + } + + @Configuration + @EnableRabbit + public static class TestConfiguration { + + @Bean + CachingConnectionFactory connectionFactory() { + CachingConnectionFactory connectionFactory = + new CachingConnectionFactory("localhost", RABBIT_MQ_CONTAINER.getAmqpPort()); + connectionFactory.setChannelCacheSize(1); + connectionFactory.setChannelCheckoutTimeout(2000); + return connectionFactory; + } + + @Bean + CachingConnectionFactory publisherConnectionFactory() { + CachingConnectionFactory connectionFactory = + new CachingConnectionFactory("localhost", RABBIT_MQ_CONTAINER.getAmqpPort()); + connectionFactory.setChannelCacheSize(1); + connectionFactory.setChannelCheckoutTimeout(2000); + return connectionFactory; + } + + @Bean + RabbitTemplate rabbitTemplate() { + return new RabbitTemplate(publisherConnectionFactory()); + } + + @Bean + SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(connectionFactory()); + return factory; + } + + @Bean + RabbitAdmin rabbitAdmin() { + return new RabbitAdmin(publisherConnectionFactory()); + } + + BlockingQueue received = new LinkedBlockingQueue<>(); + + @RabbitListener(queuesToDeclare = @Queue("testQueue")) + void consume(String payload) { + this.received.add(payload); + } + + } + +} + From ed2cddb92a4be0e6a5db71d8b9a85e3b063645ae Mon Sep 17 00:00:00 2001 From: Claudio Silva Junior <42524939+Claudio-code@users.noreply.github.com> Date: Thu, 14 Dec 2023 14:50:37 -0300 Subject: [PATCH 323/737] Fix broken links in docs --- src/api/overview.html | 2 +- .../antora/modules/ROOT/pages/amqp/broker-configuration.adoc | 2 +- src/reference/antora/modules/ROOT/pages/amqp/connections.adoc | 2 +- .../antora/modules/ROOT/pages/amqp/listener-queues.adoc | 2 +- .../receiving-messages/async-annotation-driven/enable.adoc | 2 +- .../antora/modules/ROOT/pages/amqp/request-reply.adoc | 4 ++-- .../antora/modules/ROOT/pages/amqp/sending-messages.adoc | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/api/overview.html b/src/api/overview.html index a7ff43cd32..fe4739a2cf 100644 --- a/src/api/overview.html +++ b/src/api/overview.html @@ -4,7 +4,7 @@

- For further API reference and developer documentation, see the Spring AMQP reference documentation. + For further API reference and developer documentation, see the Spring AMQP reference documentation. That documentation contains more detailed, developer-targeted descriptions, with conceptual overviews, definitions of terms, workarounds, and working code examples.

diff --git a/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc b/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc index e3eb3bf916..6de2afaa41 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc @@ -359,7 +359,7 @@ public Exchange exchange() { } ---- -See the Javadoc for https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/core/QueueBuilder.html[`org.springframework.amqp.core.QueueBuilder`] and https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/core/ExchangeBuilder.html[`org.springframework.amqp.core.ExchangeBuilder`] for more information. +See the Javadoc for https://docs.spring.io/spring-amqp/docs/current/api/org/springframework/amqp/core/QueueBuilder.html[`org.springframework.amqp.core.QueueBuilder`] and https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/core/ExchangeBuilder.html[`org.springframework.amqp.core.ExchangeBuilder`] for more information. Starting with version 2.0, the `ExchangeBuilder` now creates durable exchanges by default, to be consistent with the simple constructors on the individual `AbstractExchange` classes. To make a non-durable exchange with the builder, use `.durable(false)` before invoking `.build()`. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc b/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc index d2199ecddd..a8bf47327f 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc @@ -480,7 +480,7 @@ public class MyService { ---- It is important to unbind the resource after use. -For more information, see the https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.html[JavaDoc] for `AbstractRoutingConnectionFactory`. +For more information, see the https://docs.spring.io/spring-amqp/docs/current/api/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.html[JavaDoc] for `AbstractRoutingConnectionFactory`. Starting with version 1.4, `RabbitTemplate` supports the SpEL `sendConnectionFactorySelectorExpression` and `receiveConnectionFactorySelectorExpression` properties, which are evaluated on each AMQP protocol interaction operation (`send`, `sendAndReceive`, `receive`, or `receiveAndReply`), resolving to a `lookupKey` value for the provided `AbstractRoutingConnectionFactory`. You can use bean references, such as `@vHostResolver.getVHost(#root)` in the expression. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/listener-queues.adoc b/src/reference/antora/modules/ROOT/pages/amqp/listener-queues.adoc index 1e52cef453..03fe292c7a 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/listener-queues.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/listener-queues.adoc @@ -8,7 +8,7 @@ Container can be initially configured to listen on zero queues. Queues can be added and removed at runtime. The `SimpleMessageListenerContainer` recycles (cancels and re-creates) all consumers when any pre-fetched messages have been processed. The `DirectMessageListenerContainer` creates/cancels individual consumer(s) for each queue without affecting consumers on other queues. -See the https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.html[Javadoc] for the `addQueues`, `addQueueNames`, `removeQueues` and `removeQueueNames` methods. +See the https://docs.spring.io/spring-amqp/docs/current/api/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.html[Javadoc] for the `addQueues`, `addQueueNames`, `removeQueues` and `removeQueueNames` methods. If not all queues are available, the container tries to passively declare (and consume from) the missing queues every 60 seconds. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/enable.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/enable.adoc index 98890bf869..71d39dc08b 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/enable.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/enable.adoc @@ -37,7 +37,7 @@ In this case, and ignoring the RabbitMQ infrastructure setup, the `processOrder` You can customize the listener container factory to use for each annotation, or you can configure an explicit default by implementing the `RabbitListenerConfigurer` interface. The default is required only if at least one endpoint is registered without a specific container factory. -See the https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/rabbit/annotation/RabbitListenerConfigurer.html[Javadoc] for full details and examples. +See the https://docs.spring.io/spring-amqp/docs/current/api/org/springframework/amqp/rabbit/annotation/RabbitListenerConfigurer.html[Javadoc] for full details and examples. The container factories provide methods for adding `MessagePostProcessor` instances that are applied after receiving messages (before invoking the listener) and before sending replies. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/request-reply.adoc b/src/reference/antora/modules/ROOT/pages/amqp/request-reply.adoc index dadca617d5..57d515d987 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/request-reply.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/request-reply.adoc @@ -6,11 +6,11 @@ Those methods are quite useful for request-reply scenarios, since they handle th Similar request-reply methods are also available where the `MessageConverter` is applied to both the request and reply. Those methods are named `convertSendAndReceive`. -See the https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/core/AmqpTemplate.html[Javadoc of `AmqpTemplate`] for more detail. +See the https://docs.spring.io/spring-amqp/docs/current/api/org/springframework/amqp/core/AmqpTemplate.html[Javadoc of `AmqpTemplate`] for more detail. Starting with version 1.5.0, each of the `sendAndReceive` method variants has an overloaded version that takes `CorrelationData`. Together with a properly configured connection factory, this enables the receipt of publisher confirms for the send side of the operation. -See xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns] and the https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/rabbit/core/RabbitOperations.html[Javadoc for `RabbitOperations`] for more information. +See xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns] and the https://docs.spring.io/spring-amqp/docs/current/api/org/springframework/amqp/rabbit/core/RabbitOperations.html[Javadoc for `RabbitOperations`] for more information. Starting with version 2.0, there are variants of these methods (`convertSendAndReceiveAsType`) that take an additional `ParameterizedTypeReference` argument to convert complex returned types. The template must be configured with a `SmartMessageConverter`. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/sending-messages.adoc b/src/reference/antora/modules/ROOT/pages/amqp/sending-messages.adoc index 64188a386f..961b8daa13 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/sending-messages.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/sending-messages.adoc @@ -100,7 +100,7 @@ Message message = MessageBuilder.withBody("foo".getBytes()) .build(); ---- -Each of the properties defined on the https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/core/MessageProperties.html[`MessageProperties`] can be set. +Each of the properties defined on the https://docs.spring.io/spring-amqp/docs/current/api/org/springframework/amqp/core/MessageProperties.html[`MessageProperties`] can be set. Other methods include `setHeader(String key, String value)`, `removeHeader(String key)`, `removeHeaders()`, and `copyProperties(MessageProperties properties)`. Each property setting method has a `set*IfAbsent()` variant. In the cases where a default initial value exists, the method is named `set*IfAbsentOrDefault()`. From c3672dd10282938b1ba3e01aa67db2f26b0801b6 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 15 Dec 2023 14:32:30 -0500 Subject: [PATCH 324/737] GH-2568: Get rid of synchronized consumersMonitor Fixes: #2568 Rework all the `synchronized (this.consumersMonitor)` in the `AbstractMessageListenerContainer` hierarchy for the `consumersLock.lock()/unlock()` --- .../AbstractMessageListenerContainer.java | 17 +++- .../listener/BlockingQueueConsumer.java | 2 +- .../DirectMessageListenerContainer.java | 77 ++++++++++++----- ...DirectReplyToMessageListenerContainer.java | 20 ++++- .../SimpleMessageListenerContainer.java | 82 +++++++++++++++---- 5 files changed, 153 insertions(+), 45 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index 75a454dd3e..00717550b3 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -29,6 +29,8 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import org.aopalliance.aop.Advice; @@ -131,7 +133,7 @@ public abstract class AbstractMessageListenerContainer extends ObservableListene private final ContainerDelegate delegate = this::actualInvokeListener; - protected final Object consumersMonitor = new Object(); //NOSONAR + protected final Lock consumersLock = new ReentrantLock(); //NOSONAR private final Map consumerArgs = new HashMap<>(); @@ -253,6 +255,7 @@ public void setApplicationEventPublisher(ApplicationEventPublisher applicationEv this.applicationEventPublisher = applicationEventPublisher; } + @Nullable protected ApplicationEventPublisher getApplicationEventPublisher() { return this.applicationEventPublisher; } @@ -686,10 +689,14 @@ protected ConsumerTagStrategy getConsumerTagStrategy() { * @since 1.3 */ public void setConsumerArguments(Map args) { - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { this.consumerArgs.clear(); this.consumerArgs.putAll(args); } + finally { + this.consumersLock.unlock(); + } } /** @@ -698,9 +705,13 @@ public void setConsumerArguments(Map args) { * @since 2.0 */ public Map getConsumerArguments() { - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { return new HashMap<>(this.consumerArgs); } + finally { + this.consumersLock.unlock(); + } } /** diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java index c6a95816af..a23e77e67f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java @@ -379,7 +379,7 @@ public void setLocallyTransacted(boolean locallyTransacted) { this.locallyTransacted = locallyTransacted; } - public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + public void setApplicationEventPublisher(@Nullable ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java index ee3a02c7ad..3f1e21b229 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java @@ -62,6 +62,7 @@ import org.springframework.amqp.rabbit.listener.support.ContainerUtils; import org.springframework.amqp.rabbit.support.ActiveObjectCounter; import org.springframework.amqp.rabbit.transaction.RabbitTransactionManager; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.lang.Nullable; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; @@ -279,7 +280,8 @@ public void addQueues(Queue... queues) { private void addQueues(Stream queueNameStream) { if (isRunning()) { - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { checkStartState(); Set current = getQueueNamesAsSet(); queueNameStream.forEach(queue -> { @@ -292,6 +294,9 @@ private void addQueues(Stream queueNameStream) { } }); } + finally { + this.consumersLock.unlock(); + } } } @@ -309,7 +314,8 @@ public boolean removeQueues(Queue... queues) { private void removeQueues(Stream queueNames) { if (isRunning()) { - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { checkStartState(); queueNames.map(queue -> { this.removedQueues.add(queue); @@ -319,11 +325,15 @@ private void removeQueues(Stream queueNames) { .flatMap(Collection::stream) .forEach(this::cancelConsumer); } + finally { + this.consumersLock.unlock(); + } } } private void adjustConsumers(int newCount) { - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { checkStartState(); this.consumersToRestart.clear(); for (String queue : getQueueNames()) { @@ -334,9 +344,9 @@ private void adjustConsumers(int newCount) { if (cBQ != null) { // find a gap or set the index to the end List indices = cBQ.stream() - .map(cons -> cons.getIndex()) + .map(SimpleConsumer::getIndex) .sorted() - .collect(Collectors.toList()); + .toList(); for (index = 0; index < indices.size(); index++) { if (index < indices.get(index)) { break; @@ -348,6 +358,9 @@ private void adjustConsumers(int newCount) { reduceConsumersIfIdle(newCount, queue); } } + finally { + this.consumersLock.unlock(); + } } private void reduceConsumersIfIdle(int newCount, String queue) { @@ -367,9 +380,8 @@ private void reduceConsumersIfIdle(int newCount, String queue) { } /** - * When adjusting down, return a consumer that can be canceled. Called while - * synchronized on consumersMonitor. - * @return the consumer index or -1 if non idle. + * When adjusting down, return a consumer that can be canceled. Called while locked on {@link #consumersLock}. + * @return the consumer index or -1 if non-idle. * @since 2.0.6 */ protected int findIdleConsumer() { @@ -482,11 +494,12 @@ private void startMonitor(long idleEventInterval, final Map names checkIdle(idleEventInterval, now); checkConsumers(now); if (this.lastRestartAttempt + getFailedDeclarationRetryInterval() < now) { - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { if (this.started) { List restartableConsumers = new ArrayList<>(this.consumersToRestart); this.consumersToRestart.clear(); - if (restartableConsumers.size() > 0) { + if (!restartableConsumers.isEmpty()) { doRedeclareElementsIfNecessary(); } Iterator iterator = restartableConsumers.iterator(); @@ -509,6 +522,9 @@ private void startMonitor(long idleEventInterval, final Map names this.lastRestartAttempt = now; } } + finally { + this.consumersLock.unlock(); + } } processMonitorTask(); }, Duration.ofMillis(this.monitorInterval)); @@ -524,7 +540,8 @@ private void checkIdle(long idleEventInterval, long now) { private void checkConsumers(long now) { final List consumersToCancel; - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { consumersToCancel = this.consumers.stream() .filter(consumer -> { boolean open = consumer.getChannel().isOpen() && !consumer.isAckFailed() @@ -541,6 +558,9 @@ private void checkConsumers(long now) { }) .collect(Collectors.toList()); } + finally { + this.consumersLock.unlock(); + } consumersToCancel .forEach(consumer -> { try { @@ -591,7 +611,8 @@ private boolean restartConsumer(final Map namesToQueues, List canceledConsumers = null; boolean waitForConsumers = false; - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { if (this.started || this.aborted) { // Copy in the same order to avoid ConcurrentModificationException during remove in the // cancelConsumer(). @@ -823,6 +853,9 @@ protected void shutdownAndWaitOrCallback(@Nullable Runnable callback) { waitForConsumers = true; } } + finally { + this.consumersLock.unlock(); + } if (waitForConsumers) { LinkedList consumersToWait = canceledConsumers; Runnable awaitShutdown = () -> { @@ -872,7 +905,7 @@ private void runCallbackIfNotNull(@Nullable Runnable callback) { } /** - * Must hold this.consumersMonitor. + * Must hold this.consumersLock. * @param consumers a copy of this.consumers. */ private void actualShutDown(List consumers) { @@ -1005,7 +1038,7 @@ public String getConsumerTag() { } /** - * Return the current epoch for this consumer; consumersMonitor must be held. + * Return the current epoch for this consumer; consumersLock must be held. * @return the epoch. */ int getEpoch() { @@ -1039,7 +1072,7 @@ boolean targetChanged() { } /** - * Increment and return the current epoch for this consumer; consumersMonitor must + * Increment and return the current epoch for this consumer; consumersLock must * be held. * @return the epoch. */ @@ -1323,7 +1356,8 @@ public void handleCancel(String consumerTag) { void cancelConsumer(final String eventMessage) { publishConsumerFailedEvent(eventMessage, true, null); - synchronized (DirectMessageListenerContainer.this.consumersMonitor) { + DirectMessageListenerContainer.this.consumersLock.lock(); + try { List list = DirectMessageListenerContainer.this.consumersByQueue.get(this.queue); if (list != null) { list.remove(this); @@ -1331,6 +1365,9 @@ void cancelConsumer(final String eventMessage) { DirectMessageListenerContainer.this.consumers.remove(this); addConsumerToRestart(this); } + finally { + DirectMessageListenerContainer.this.consumersLock.unlock(); + } finalizeConsumer(); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainer.java index 7819f4d202..b639e11e33 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2023 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. @@ -118,7 +118,8 @@ protected void doStart() { @Override protected void processMonitorTask() { long now = System.currentTimeMillis(); - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { long reduce = this.consumers.stream() .filter(c -> this.whenUsed.containsKey(c) && !this.inUseConsumerChannels.containsValue(c) && this.whenUsed.get(c) < now - getIdleEventInterval()) @@ -131,6 +132,9 @@ protected void processMonitorTask() { super.setConsumersPerQueue(this.consumerCount); } } + finally { + this.consumersLock.unlock(); + } } @Override @@ -155,7 +159,8 @@ protected void consumerRemoved(SimpleConsumer consumer) { * @return the channel holder. */ public ChannelHolder getChannelHolder() { - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { ChannelHolder channelHolder = null; while (channelHolder == null) { if (!isRunning()) { @@ -177,6 +182,9 @@ public ChannelHolder getChannelHolder() { } return channelHolder; } + finally { + this.consumersLock.unlock(); + } } /** @@ -188,7 +196,8 @@ public ChannelHolder getChannelHolder() { * @param message a message to be included in the cancel event if cancelConsumer is true. */ public void releaseConsumerFor(ChannelHolder channelHolder, boolean cancelConsumer, @Nullable String message) { - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { SimpleConsumer consumer = this.inUseConsumerChannels.get(channelHolder.getChannel()); if (consumer != null && consumer.getEpoch() == channelHolder.getConsumerEpoch()) { this.inUseConsumerChannels.remove(channelHolder.getChannel()); @@ -198,6 +207,9 @@ public void releaseConsumerFor(ChannelHolder channelHolder, boolean cancelConsum } } } + finally { + this.consumersLock.unlock(); + } } /** diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java index af48868e5c..91325fde86 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java @@ -171,7 +171,8 @@ public void setConcurrentConsumers(final int concurrentConsumers) { Assert.isTrue(concurrentConsumers <= this.maxConcurrentConsumers, "'concurrentConsumers' cannot be more than 'maxConcurrentConsumers'"); } - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { if (logger.isDebugEnabled()) { logger.debug("Changing consumers from " + this.concurrentConsumers + " to " + concurrentConsumers); } @@ -181,6 +182,9 @@ public void setConcurrentConsumers(final int concurrentConsumers) { adjustConsumers(delta); } } + finally { + this.consumersLock.unlock(); + } } /** @@ -532,11 +536,12 @@ public int getActiveConsumerCount() { @Override protected void doStart() { Assert.state(!this.consumerBatchEnabled || getMessageListener() instanceof BatchMessageListener - || getMessageListener() instanceof ChannelAwareBatchMessageListener, + || getMessageListener() instanceof ChannelAwareBatchMessageListener, "When setting 'consumerBatchEnabled' to true, the listener must support batching"); checkListenerContainerAware(); super.doStart(); - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { if (this.consumers != null) { throw new IllegalStateException("A stopped container should not have consumers"); } @@ -552,7 +557,7 @@ protected void doStart() { } return; } - Set processors = new HashSet(); + Set processors = new HashSet<>(); for (BlockingQueueConsumer consumer : this.consumers) { AsyncMessageProcessingConsumer processor = new AsyncMessageProcessingConsumer(consumer); processors.add(processor); @@ -563,6 +568,9 @@ protected void doStart() { } waitForConsumersToStart(processors); } + finally { + this.consumersLock.unlock(); + } } private void checkListenerContainerAware() { @@ -616,7 +624,8 @@ protected void shutdownAndWaitOrCallback(@Nullable Runnable callback) { } List canceledConsumers = new ArrayList<>(); - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { if (this.consumers != null) { Iterator consumerIterator = this.consumers.iterator(); if (isForceStop()) { @@ -640,6 +649,9 @@ protected void shutdownAndWaitOrCallback(@Nullable Runnable callback) { return; } } + finally { + this.consumersLock.unlock(); + } Runnable awaitShutdown = () -> { logger.info("Waiting for workers to finish."); @@ -665,10 +677,14 @@ protected void shutdownAndWaitOrCallback(@Nullable Runnable callback) { logger.warn("Interrupted waiting for workers. Continuing with shutdown."); } - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { this.consumers = null; this.cancellationLock.deactivate(); } + finally { + this.consumersLock.unlock(); + } this.stopNow.set(false); runCallbackIfNotNull(callback); }; @@ -688,18 +704,23 @@ private void runCallbackIfNotNull(@Nullable Runnable callback) { private boolean isActive(BlockingQueueConsumer consumer) { boolean consumerActive; - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { consumerActive = this.consumers != null && this.consumers.contains(consumer); } + finally { + this.consumersLock.unlock(); + } return consumerActive && this.isActive(); } protected int initializeConsumers() { int count = 0; - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { if (this.consumers == null) { this.cancellationLock.reset(); - this.consumers = new HashSet(this.concurrentConsumers); + this.consumers = new HashSet<>(this.concurrentConsumers); for (int i = 1; i <= this.concurrentConsumers; i++) { BlockingQueueConsumer consumer = createBlockingQueueConsumer(); if (getConsumeDelay() > 0) { @@ -710,6 +731,9 @@ protected int initializeConsumers() { } } } + finally { + this.consumersLock.unlock(); + } return count; } @@ -720,13 +744,14 @@ protected int initializeConsumers() { */ protected void adjustConsumers(int deltaArg) { int delta = deltaArg; - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { if (isActive() && this.consumers != null) { if (delta > 0) { Iterator consumerIterator = this.consumers.iterator(); while (consumerIterator.hasNext() && delta > 0 - && (this.maxConcurrentConsumers == null - || this.consumers.size() > this.maxConcurrentConsumers)) { + && (this.maxConcurrentConsumers == null + || this.consumers.size() > this.maxConcurrentConsumers)) { BlockingQueueConsumer consumer = consumerIterator.next(); consumer.basicCancel(true); consumerIterator.remove(); @@ -738,6 +763,9 @@ protected void adjustConsumers(int deltaArg) { } } } + finally { + this.consumersLock.unlock(); + } } @@ -746,7 +774,8 @@ protected void adjustConsumers(int deltaArg) { * @param delta the consumers to add. */ protected void addAndStartConsumers(int delta) { - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { if (this.consumers != null) { for (int i = 0; i < delta; i++) { if (this.maxConcurrentConsumers != null @@ -782,10 +811,14 @@ protected void addAndStartConsumers(int delta) { } } } + finally { + this.consumersLock.unlock(); + } } private void considerAddingAConsumer() { - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { if (this.consumers != null && this.maxConcurrentConsumers != null && this.consumers.size() < this.maxConcurrentConsumers) { long now = System.currentTimeMillis(); @@ -795,11 +828,15 @@ private void considerAddingAConsumer() { } } } + finally { + this.consumersLock.unlock(); + } } private void considerStoppingAConsumer(BlockingQueueConsumer consumer) { - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { if (this.consumers != null && this.consumers.size() > this.concurrentConsumers) { long now = System.currentTimeMillis(); if (this.lastConsumerStopped + this.stopConsumerMinInterval < now) { @@ -812,10 +849,14 @@ private void considerStoppingAConsumer(BlockingQueueConsumer consumer) { } } } + finally { + this.consumersLock.unlock(); + } } private void queuesChanged() { - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { if (this.consumers != null) { int count = 0; Iterator consumerIterator = this.consumers.iterator(); @@ -837,6 +878,9 @@ private void queuesChanged() { addAndStartConsumers(count); } } + finally { + this.consumersLock.unlock(); + } } protected BlockingQueueConsumer createBlockingQueueConsumer() { @@ -872,7 +916,8 @@ this.cancellationLock, getAcknowledgeMode(), isChannelTransacted(), actualPrefet private void restart(BlockingQueueConsumer oldConsumer) { BlockingQueueConsumer consumer = oldConsumer; - synchronized (this.consumersMonitor) { + this.consumersLock.lock(); + try { if (this.consumers != null) { try { // Need to recycle the channel in this consumer @@ -904,6 +949,9 @@ private void restart(BlockingQueueConsumer oldConsumer) { .execute(new AsyncMessageProcessingConsumer(consumer)); } } + finally { + this.consumersLock.unlock(); + } } private boolean receiveAndExecute(final BlockingQueueConsumer consumer) throws Exception { // NOSONAR From 9fe0ef21b3163c9bd722ea494592bed8e9a83cdd Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 15 Dec 2023 17:02:35 -0500 Subject: [PATCH 325/737] GH-2573: Get rid of synchronized in the CCF Fixes: #2573 Rework all the `synchronized` blocks in the `CachingConnectionFactory` to `Lock` and `Condition` --- .../connection/CachingConnectionFactory.java | 295 +++++++++++------- .../PublisherCallbackChannelImpl.java | 2 +- 2 files changed, 178 insertions(+), 119 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java index 8ae4758796..7eff49b839 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java @@ -24,6 +24,7 @@ import java.net.URI; import java.util.Arrays; import java.util.Collection; +import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; @@ -44,6 +45,9 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.springframework.amqp.AmqpApplicationContextClosedException; import org.springframework.amqp.AmqpException; @@ -115,13 +119,11 @@ public class CachingConnectionFactory extends AbstractConnectionFactory */ private static final AtomicInteger threadPoolId = new AtomicInteger(); // NOSONAR lower case - private static final Set txStarts = new HashSet<>(Arrays.asList("basicPublish", "basicAck", // NOSONAR - "basicNack", "basicReject")); + private static final Set txStarts = Set.of("basicPublish", "basicAck", "basicNack", "basicReject"); - private static final Set ackMethods = new HashSet<>(Arrays.asList("basicAck", // NOSONAR - "basicNack", "basicReject")); + private static final Set ackMethods = Set.of("basicAck", "basicNack", "basicReject"); - private static final Set txEnds = new HashSet<>(Arrays.asList("txCommit", "txRollback")); // NOSONAR + private static final Set txEnds = Set.of("txCommit", "txRollback"); private final ChannelCachingConnectionProxy connection = new ChannelCachingConnectionProxy(null); @@ -148,14 +150,13 @@ public enum CacheMode { public enum ConfirmType { /** - * Use {@code RabbitTemplate#waitForConfirms()} (or {@code waitForConfirmsOrDie()} + * Use {@code RabbitTemplate#waitForConfirms()} or {@code waitForConfirmsOrDie()} * within scoped operations. */ SIMPLE, /** - * Use with {@code CorrelationData} to correlate confirmations with sent - * messsages. + * Use with {@code CorrelationData} to correlate confirmations with sent messages. */ CORRELATED, @@ -176,9 +177,9 @@ public enum ConfirmType { private final BlockingDeque idleConnections = new LinkedBlockingDeque<>(); - private final LinkedList cachedChannelsNonTransactional = new LinkedList<>(); // NOSONAR removeFirst() + private final Deque cachedChannelsNonTransactional = new LinkedList<>(); // NOSONAR removeFirst() - private final LinkedList cachedChannelsTransactional = new LinkedList<>(); // NOSONAR removeFirst() + private final Deque cachedChannelsTransactional = new LinkedList<>(); // NOSONAR removeFirst() private final Map checkoutPermits = new HashMap<>(); @@ -186,8 +187,13 @@ public enum ConfirmType { private final AtomicInteger connectionHighWaterMark = new AtomicInteger(); - /** Synchronization monitor for the shared Connection. */ - private final Object connectionMonitor = new Object(); + /** + * Synchronization lock for the shared Connection. + */ + private final Lock connectionLock = new ReentrantLock(); + + private final Condition connectionAvailableCondition = this.connectionLock.newCondition(); + private final ActiveObjectCounter inFlightAsyncCloses = new ActiveObjectCounter<>(); @@ -295,14 +301,16 @@ private CachingConnectionFactory(com.rabbitmq.client.ConnectionFactory rabbitCon if (!isPublisherFactory) { if (rabbitConnectionFactory.isAutomaticRecoveryEnabled()) { rabbitConnectionFactory.setAutomaticRecoveryEnabled(false); - logger.warn("***\nAutomatic Recovery was Enabled in the provided connection factory;\n" - + "while Spring AMQP is generally compatible with this feature, there\n" - + "are some corner cases where problems arise. Spring AMQP\n" - + "prefers to use its own recovery mechanisms; when this option is true, you may receive\n" - + "'AutoRecoverConnectionNotCurrentlyOpenException's until the connection is recovered.\n" - + "It has therefore been disabled; if you really wish to enable it, use\n" - + "'getRabbitConnectionFactory().setAutomaticRecoveryEnabled(true)',\n" - + "but this is discouraged."); + logger.warn(""" + *** + Automatic Recovery was Enabled in the provided connection factory; + while Spring AMQP is generally compatible with this feature, there + are some corner cases where problems arise. Spring AMQP + prefers to use its own recovery mechanisms; when this option is true, you may receive + 'AutoRecoverConnectionNotCurrentlyOpenException's until the connection is recovered. + It has therefore been disabled; if you really wish to enable it, use + 'getRabbitConnectionFactory().setAutomaticRecoveryEnabled(true)', + but this is discouraged."""); } super.setPublisherConnectionFactory(new CachingConnectionFactory(getRabbitConnectionFactory(), true)); } @@ -513,12 +521,12 @@ private Channel getChannel(ChannelCachingConnectionProxy connection, boolean tra if (this.channelCheckoutTimeout > 0) { permits = obtainPermits(connection); } - LinkedList channelList = determineChannelList(connection, transactional); + Deque channelList = determineChannelList(connection, transactional); ChannelProxy channel = null; if (connection.isOpen()) { - channel = findOpenChannel(channelList); + channel = findOpenChannel(channelList, connection.channelListLock); if (channel != null && logger.isTraceEnabled()) { - logger.trace("Found cached Rabbit Channel: " + channel.toString()); + logger.trace("Found cached Rabbit Channel: " + channel); } } if (channel == null) { @@ -564,10 +572,10 @@ private Semaphore obtainPermits(ChannelCachingConnectionProxy connection) { } @Nullable - private ChannelProxy findOpenChannel(LinkedList channelList) { // NOSONAR - LL Vs. L - removeFirst() - + private ChannelProxy findOpenChannel(Deque channelList, Lock channelListLock) { ChannelProxy channel = null; - synchronized (channelList) { + channelListLock.lock(); + try { while (!channelList.isEmpty()) { channel = channelList.removeFirst(); if (logger.isTraceEnabled()) { @@ -582,6 +590,9 @@ private ChannelProxy findOpenChannel(LinkedList channelList) { // } } } + finally { + channelListLock.unlock(); + } return channel; } @@ -613,12 +624,10 @@ private void cleanUpClosedChannel(ChannelProxy channel) { } } - private LinkedList determineChannelList(ChannelCachingConnectionProxy connection, // NOSONAR LL - boolean transactional) { - LinkedList channelList; // NOSONAR must be LinkedList + private Deque determineChannelList(ChannelCachingConnectionProxy connection, boolean transactional) { + Deque channelList; if (this.cacheMode == CacheMode.CHANNEL) { - channelList = transactional ? this.cachedChannelsTransactional - : this.cachedChannelsNonTransactional; + channelList = transactional ? this.cachedChannelsTransactional : this.cachedChannelsNonTransactional; } else { channelList = transactional ? this.allocatedConnectionTransactionalChannels.get(connection) @@ -631,7 +640,7 @@ private LinkedList determineChannelList(ChannelCachingConnectionPr } private ChannelProxy getCachedChannelProxy(ChannelCachingConnectionProxy connection, - LinkedList channelList, boolean transactional) { //NOSONAR LinkedList for addLast() + Deque channelList, boolean transactional) { Channel targetChannel = createBareChannel(connection, transactional); if (logger.isDebugEnabled()) { @@ -640,10 +649,10 @@ private ChannelProxy getCachedChannelProxy(ChannelCachingConnectionProxy connect getChannelListener().onCreate(targetChannel, transactional); Class[] interfaces; if (ConfirmType.CORRELATED.equals(this.confirmType) || this.publisherReturns) { - interfaces = new Class[] { ChannelProxy.class, PublisherCallbackChannel.class }; + interfaces = new Class[] {ChannelProxy.class, PublisherCallbackChannel.class}; } else { - interfaces = new Class[] { ChannelProxy.class }; + interfaces = new Class[] {ChannelProxy.class}; } return (ChannelProxy) Proxy.newProxyInstance(ChannelProxy.class.getClassLoader(), interfaces, new CachedChannelInvocationHandler(connection, targetChannel, channelList, @@ -653,7 +662,8 @@ interfaces, new CachedChannelInvocationHandler(connection, targetChannel, channe private Channel createBareChannel(ChannelCachingConnectionProxy connection, boolean transactional) { if (this.cacheMode == CacheMode.CHANNEL) { if (!this.connection.isOpen()) { - synchronized (this.connectionMonitor) { + this.connectionLock.lock(); + try { if (!this.connection.isOpen()) { this.connection.notifyCloseIfNecessary(); } @@ -662,17 +672,24 @@ private Channel createBareChannel(ChannelCachingConnectionProxy connection, bool createConnection(); } } + finally { + this.connectionLock.unlock(); + } } return doCreateBareChannel(this.connection, transactional); } else if (this.cacheMode == CacheMode.CONNECTION) { if (!connection.isOpen()) { - synchronized (this.connectionMonitor) { + this.connectionLock.lock(); + try { this.allocatedConnectionNonTransactionalChannels.get(connection).clear(); this.allocatedConnectionTransactionalChannels.get(connection).clear(); connection.notifyCloseIfNecessary(); refreshProxyConnection(connection); } + finally { + this.connectionLock.unlock(); + } } return doCreateBareChannel(connection, transactional); } @@ -693,9 +710,7 @@ private Channel doCreateBareChannel(ChannelCachingConnectionProxy conn, boolean && !(channel instanceof PublisherCallbackChannelImpl)) { channel = this.publisherChannelFactory.createChannel(channel, getChannelsExecutor()); } - if (channel != null) { - channel.addShutdownListener(this); - } + channel.addShutdownListener(this); return channel; // NOSONAR - Simple connection throws exception } @@ -705,7 +720,8 @@ public final Connection createConnection() throws AmqpException { throw new AmqpApplicationContextClosedException( "The ApplicationContext is closed and the ConnectionFactory can no longer create connections."); } - synchronized (this.connectionMonitor) { + this.connectionLock.lock(); + try { if (this.cacheMode == CacheMode.CHANNEL) { if (this.connection.target == null) { this.connection.target = super.createBareConnection(); @@ -722,6 +738,9 @@ else if (this.cacheMode == CacheMode.CONNECTION) { return connectionFromCache(); } } + finally { + this.connectionLock.unlock(); + } return null; // NOSONAR - never reach here - exceptions } @@ -741,10 +760,10 @@ private Connection connectionFromCache() { logger.debug("Adding new connection '" + cachedConnection + "'"); } this.allocatedConnections.add(cachedConnection); - this.allocatedConnectionNonTransactionalChannels.put(cachedConnection, new LinkedList()); + this.allocatedConnectionNonTransactionalChannels.put(cachedConnection, new LinkedList<>()); this.channelHighWaterMarks.put(ObjectUtils.getIdentityHexString( this.allocatedConnectionNonTransactionalChannels.get(cachedConnection)), new AtomicInteger()); - this.allocatedConnectionTransactionalChannels.put(cachedConnection, new LinkedList()); + this.allocatedConnectionTransactionalChannels.put(cachedConnection, new LinkedList<>()); this.channelHighWaterMarks.put( ObjectUtils .getIdentityHexString(this.allocatedConnectionTransactionalChannels.get(cachedConnection)), @@ -774,8 +793,9 @@ private ChannelCachingConnectionProxy waitForConnection(long now) { while (cachedConnection == null && System.currentTimeMillis() - now < this.channelCheckoutTimeout) { if (countOpenConnections() >= this.connectionLimit) { try { - this.connectionMonitor.wait(this.channelCheckoutTimeout); - cachedConnection = findIdleConnection(); + if (this.connectionAvailableCondition.await(this.channelCheckoutTimeout, TimeUnit.MILLISECONDS)) { + cachedConnection = findIdleConnection(); + } } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -805,7 +825,7 @@ private ChannelCachingConnectionProxy findIdleConnection() { cachedConnection.notifyCloseIfNecessary(); this.idleConnections.addLast(cachedConnection); if (cachedConnection.equals(lastIdle)) { - // all of the idle connections are closed. + // all the idled connections are closed. cachedConnection = this.idleConnections.poll(); break; } @@ -846,7 +866,8 @@ public final void destroy() { resetConnection(); if (getContextStopped()) { this.stopped = true; - synchronized (this.connectionMonitor) { + this.connectionLock.lock(); + try { if (this.channelsExecutor != null) { try { if (!this.inFlightAsyncCloses.await(CHANNEL_EXEC_SHUTDOWN_TIMEOUT, TimeUnit.SECONDS)) { @@ -866,6 +887,9 @@ public final void destroy() { } } } + finally { + this.connectionLock.unlock(); + } } } @@ -877,23 +901,27 @@ public final void destroy() { */ @Override public void resetConnection() { - synchronized (this.connectionMonitor) { + this.connectionLock.lock(); + try { if (this.connection.target != null) { this.connection.destroy(); } - this.allocatedConnections.forEach(c -> c.destroy()); + this.allocatedConnections.forEach(ChannelCachingConnectionProxy::destroy); this.channelHighWaterMarks.values().forEach(count -> count.set(0)); this.connectionHighWaterMark.set(0); } + finally { + this.connectionLock.unlock(); + } if (this.defaultPublisherFactory) { - ((CachingConnectionFactory) getPublisherConnectionFactory()).resetConnection(); // NOSONAR + getPublisherConnectionFactory().resetConnection(); // NOSONAR } } /* * Reset the Channel cache and underlying shared Connection, to be reinitialized on next access. */ - protected void reset(List channels, List txChannels, + protected void reset(Deque channels, Deque txChannels, Map channelsAwaitingAcks) { this.active = false; @@ -905,10 +933,8 @@ protected void reset(List channels, List txChannels, } protected void closeAndClear(Collection theChannels) { - synchronized (theChannels) { - closeChannels(theChannels); - theChannels.clear(); - } + closeChannels(theChannels); + theChannels.clear(); } protected void closeChannels(Collection theChannels) { @@ -926,7 +952,8 @@ protected void closeChannels(Collection theChannels) { public Properties getCacheProperties() { Properties props = new Properties(); props.setProperty("cacheMode", this.cacheMode.name()); - synchronized (this.connectionMonitor) { + this.connectionLock.lock(); + try { props.setProperty("channelCacheSize", Integer.toString(this.channelCacheSize)); if (this.cacheMode.equals(CacheMode.CONNECTION)) { props.setProperty("connectionCacheSize", Integer.toString(this.connectionCacheSize)); @@ -969,6 +996,9 @@ public Properties getCacheProperties() { putConnectionName(props, this.connection, ""); } } + finally { + this.connectionLock.unlock(); + } return props; } @@ -1018,7 +1048,8 @@ protected ExecutorService getChannelsExecutor() { return getExecutorService(); // NOSONAR never null } if (this.channelsExecutor == null) { - synchronized (this.connectionMonitor) { + this.connectionLock.lock(); + try { if (this.channelsExecutor == null) { final String threadPrefix = getBeanName() == null @@ -1028,6 +1059,9 @@ protected ExecutorService getChannelsExecutor() { this.channelsExecutor = Executors.newCachedThreadPool(threadPoolFactory); } } + finally { + this.connectionLock.unlock(); + } } return this.channelsExecutor; } @@ -1045,9 +1079,9 @@ public String toString() { } return "CachingConnectionFactory [channelCacheSize=" + this.channelCacheSize + (addresses != null - ? ", addresses=" + addresses - : (host != null ? ", host=" + host : "") - + (port > 0 ? ", port=" + port : "")) + ? ", addresses=" + addresses + : (host != null ? ", host=" + host : "") + + (port > 0 ? ", port=" + port : "")) + ", active=" + this.active + " " + super.toString() + "]"; } @@ -1058,11 +1092,11 @@ private final class CachedChannelInvocationHandler implements InvocationHandler private final ChannelCachingConnectionProxy theConnection; - private final LinkedList channelList; // NOSONAR addLast() + private final Deque channelList; private final String channelListIdentity; - private final Object targetMonitor = new Object(); + private final Lock targetLock = new ReentrantLock(); private final boolean transactional; @@ -1077,7 +1111,7 @@ private final class CachedChannelInvocationHandler implements InvocationHandler CachedChannelInvocationHandler(ChannelCachingConnectionProxy connection, Channel target, - LinkedList channelList, // NOSONAR addLast() + Deque channelList, boolean transactional) { this.theConnection = connection; @@ -1089,11 +1123,11 @@ private final class CachedChannelInvocationHandler implements InvocationHandler @Override // NOSONAR complexity public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // NOSONAR NCSS lines + ChannelProxy channelProxy = (ChannelProxy) proxy; if (logger.isTraceEnabled() && !method.getName().equals("toString") && !method.getName().equals("hashCode") && !method.getName().equals("equals")) { try { - logger.trace(this.target + " channel." + method.getName() + "(" - + (args != null ? Arrays.toString(args) : "") + ")"); + logger.trace(this.target + " channel." + method.getName() + "(" + Arrays.toString(args) + ")"); } catch (Exception e) { // empty - some mocks fail here @@ -1103,44 +1137,46 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl if (methodName.equals("txSelect") && !this.transactional) { throw new UnsupportedOperationException("Cannot start transaction on non-transactional channel"); } - if (methodName.equals("equals")) { - // Only consider equal when proxies are identical. - return (proxy == args[0]); // NOSONAR - } - else if (methodName.equals("hashCode")) { - // Use hashCode of Channel proxy. - return System.identityHashCode(proxy); - } - else if (methodName.equals("toString")) { - return "Cached Rabbit Channel: " + this.target + ", conn: " + this.theConnection; - } - else if (methodName.equals("close")) { - // Handle close method: don't pass the call on. - if (CachingConnectionFactory.this.active && !RabbitUtils.isPhysicalCloseRequired()) { - logicalClose((ChannelProxy) proxy); - return null; + switch (methodName) { + case "equals" -> { + // Only consider equal when proxies are identical. + return (channelProxy == args[0]); // NOSONAR } - else { - physicalClose(proxy); - return null; + case "hashCode" -> { + // Use hashCode of Channel proxy. + return System.identityHashCode(channelProxy); + } + case "toString" -> { + return "Cached Rabbit Channel: " + this.target + ", conn: " + this.theConnection; + } + case "close" -> { + // Handle close method: don't pass the call on. + if (CachingConnectionFactory.this.active && !RabbitUtils.isPhysicalCloseRequired()) { + logicalClose(channelProxy); + return null; + } + else { + physicalClose(channelProxy); + return null; + } + } + case "getTargetChannel" -> { + // Handle getTargetChannel method: return underlying Channel. + return this.target; + } + case "isOpen" -> { + // Handle isOpen method: we are closed if the target is closed + return this.target != null && this.target.isOpen(); + } + case "isTransactional" -> { + return this.transactional; + } + case "isConfirmSelected" -> { + return this.confirmSelected; + } + case "isPublisherConfirms" -> { + return this.publisherConfirms; } - } - else if (methodName.equals("getTargetChannel")) { - // Handle getTargetChannel method: return underlying Channel. - return this.target; - } - else if (methodName.equals("isOpen")) { - // Handle isOpen method: we are closed if the target is closed - return this.target != null && this.target.isOpen(); - } - else if (methodName.equals("isTransactional")) { - return this.transactional; - } - else if (methodName.equals("isConfirmSelected")) { - return this.confirmSelected; - } - else if (methodName.equals("isPublisherConfirms")) { - return this.publisherConfirms; } try { if (this.target == null || !this.target.isOpen()) { @@ -1160,7 +1196,8 @@ else if (ackMethods.contains(methodName)) { } this.target = null; } - synchronized (this.targetMonitor) { + this.targetLock.lock(); + try { if (this.target == null) { this.target = createBareChannel(this.theConnection, this.transactional); } @@ -1175,6 +1212,9 @@ else if (txEnds.contains(methodName)) { } return result; } + finally { + this.targetLock.unlock(); + } } catch (InvocationTargetException ex) { if (this.target == null || !this.target.isOpen()) { @@ -1183,28 +1223,37 @@ else if (txEnds.contains(methodName)) { logger.debug("Detected closed channel on exception. Re-initializing: " + this.target); } this.target = null; - synchronized (this.targetMonitor) { + this.targetLock.lock(); + try { if (this.target == null) { this.target = createBareChannel(this.theConnection, this.transactional); } } + finally { + this.targetLock.unlock(); + } } throw ex.getTargetException(); } } - private void releasePermitIfNecessary(Object proxy) { + private void releasePermitIfNecessary(ChannelProxy proxy) { if (CachingConnectionFactory.this.channelCheckoutTimeout > 0) { /* * Only release a permit if this is a normal close; if the channel is * in the list, it means we're closing a cached channel (for which a permit * has already been released). */ - synchronized (this.channelList) { + + this.theConnection.channelListLock.lock(); + try { if (this.channelList.contains(proxy)) { return; } } + finally { + this.theConnection.channelListLock.unlock(); + } Semaphore permits = CachingConnectionFactory.this.checkoutPermits.get(this.theConnection); if (permits != null) { permits.release(); @@ -1230,7 +1279,8 @@ private void logicalClose(ChannelProxy proxy) throws IOException, TimeoutExcepti return; } if (this.target != null && !this.target.isOpen()) { - synchronized (this.targetMonitor) { + this.targetLock.lock(); + try { if (this.target != null && !this.target.isOpen()) { if (this.target instanceof PublisherCallbackChannel) { this.target.close(); // emit nacks if necessary @@ -1245,6 +1295,9 @@ private void logicalClose(ChannelProxy proxy) throws IOException, TimeoutExcepti return; } } + finally { + this.targetLock.unlock(); + } } returnToCache(proxy); } @@ -1263,9 +1316,10 @@ private void returnToCache(ChannelProxy proxy) { } } - private void doReturnToCache(Channel proxy) { + private void doReturnToCache(@Nullable ChannelProxy proxy) { if (proxy != null) { - synchronized (this.channelList) { + this.theConnection.channelListLock.lock(); + try { // Allow for multiple close calls... if (CachingConnectionFactory.this.active) { cacheOrClose(proxy); @@ -1280,10 +1334,13 @@ private void doReturnToCache(Channel proxy) { } } } + finally { + this.theConnection.channelListLock.unlock(); + } } } - private void cacheOrClose(Channel proxy) { + private void cacheOrClose(ChannelProxy proxy) { boolean alreadyCached = this.channelList.contains(proxy); if (this.channelList.size() >= getChannelCacheSize() && !alreadyCached) { if (logger.isTraceEnabled()) { @@ -1300,7 +1357,7 @@ else if (!alreadyCached) { logger.trace("Returning cached Channel: " + this.target); } releasePermitIfNecessary(proxy); - this.channelList.addLast((ChannelProxy) proxy); + this.channelList.addLast(proxy); setHighWaterMark(); } } @@ -1317,7 +1374,7 @@ private void setHighWaterMark() { } } - private void physicalClose(Object proxy) throws IOException, TimeoutException { + private void physicalClose(ChannelProxy proxy) throws IOException, TimeoutException { if (logger.isDebugEnabled()) { logger.debug("Closing cached Channel: " + this.target); } @@ -1353,7 +1410,7 @@ private void physicalClose(Object proxy) throws IOException, TimeoutException { } } - private void asyncClose(Object proxy) { + private void asyncClose(ChannelProxy proxy) { ExecutorService executorService = getChannelsExecutor(); final Channel channel = CachedChannelInvocationHandler.this.target; CachingConnectionFactory.this.inFlightAsyncCloses.add(channel); @@ -1376,11 +1433,7 @@ private void asyncClose(Object proxy) { try { channel.close(); } - catch (@SuppressWarnings(UNUSED) IOException e3) { - } - catch (@SuppressWarnings(UNUSED) AlreadyClosedException e4) { - } - catch (@SuppressWarnings(UNUSED) TimeoutException e5) { + catch (@SuppressWarnings(UNUSED) IOException | AlreadyClosedException | TimeoutException e3) { } catch (ShutdownSignalException e6) { if (!RabbitUtils.isNormalShutdown(e6)) { @@ -1405,6 +1458,8 @@ private class ChannelCachingConnectionProxy implements ConnectionProxy { // NOSO private final AtomicBoolean closeNotified = new AtomicBoolean(false); + private final Lock channelListLock = new ReentrantLock(); + private final ConcurrentMap channelsAwaitingAcks = new ConcurrentHashMap<>(); private volatile Connection target; @@ -1438,7 +1493,8 @@ public boolean removeBlockedListener(BlockedListener listener) { @Override public void close() { if (CachingConnectionFactory.this.cacheMode == CacheMode.CONNECTION) { - synchronized (CachingConnectionFactory.this.connectionMonitor) { + CachingConnectionFactory.this.connectionLock.lock(); + try { /* * Only connectionCacheSize open idle connections are allowed. */ @@ -1459,9 +1515,12 @@ public void close() { CachingConnectionFactory.this.connectionHighWaterMark .set(CachingConnectionFactory.this.idleConnections.size()); } - CachingConnectionFactory.this.connectionMonitor.notifyAll(); + CachingConnectionFactory.this.connectionAvailableCondition.signalAll(); } } + finally { + CachingConnectionFactory.this.connectionLock.unlock(); + } } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java index b0242d86c0..c298c118a9 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java @@ -1221,7 +1221,7 @@ public String toString() { } public static PublisherCallbackChannelFactory factory() { - return (channel, exec) -> new PublisherCallbackChannelImpl(channel, exec); + return PublisherCallbackChannelImpl::new; } } From e0123429cfb1510ba4f84c45920a4533a53b4fa7 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Sat, 16 Dec 2023 09:21:28 -0500 Subject: [PATCH 326/737] GH-2574: RabbitAdmin: synchronized to Lock Fixes: #2574 * Rework all the `synchronized` blocks in the `RabbitAdmin` to `Lock` * Fix typos in Javadocs * Some other code reformatting suggested by IDE --- .../amqp/rabbit/core/RabbitAdmin.java | 106 +++++++++--------- 1 file changed, 54 insertions(+), 52 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java index 2eda92dc26..a88392b64b 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java @@ -21,7 +21,6 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; @@ -129,12 +128,14 @@ public class RabbitAdmin implements AmqpAdmin, ApplicationContextAware, Applicat private final RabbitTemplate rabbitTemplate; - private final Object lifecycleMonitor = new Object(); + private final Lock lifecycleLock = new ReentrantLock(); private final ConnectionFactory connectionFactory; private final Set manualDeclarables = Collections.synchronizedSet(new LinkedHashSet<>()); + private final Lock manualDeclarablesLock = new ReentrantLock(); + private String beanName; private RetryTemplate retryTemplate; @@ -264,20 +265,19 @@ public boolean deleteExchange(final String exchangeName) { } private void removeExchangeBindings(final String exchangeName) { - synchronized (this.manualDeclarables) { + this.manualDeclarablesLock.lock(); + try { this.manualDeclarables.stream() .filter(dec -> dec instanceof Exchange ex && ex.getName().equals(exchangeName)) .collect(Collectors.toSet()) - .forEach(ex -> this.manualDeclarables.remove(ex)); - Iterator iterator = this.manualDeclarables.iterator(); - while (iterator.hasNext()) { - Declarable next = iterator.next(); - if (next instanceof Binding binding && - ((!binding.isDestinationQueue() && binding.getDestination().equals(exchangeName)) - || binding.getExchange().equals(exchangeName))) { - iterator.remove(); - } - } + .forEach(this.manualDeclarables::remove); + this.manualDeclarables.removeIf(next -> + next instanceof Binding binding + && ((!binding.isDestinationQueue() && binding.getDestination().equals(exchangeName)) + || binding.getExchange().equals(exchangeName))); + } + finally { + this.manualDeclarablesLock.unlock(); } } @@ -295,8 +295,7 @@ private void removeExchangeBindings(final String exchangeName) { * true. */ @Override - @ManagedOperation(description = - "Declare a queue on the broker (this operation is not available remotely)") + @ManagedOperation(description = "Declare a queue on the broker (this operation is not available remotely)") @Nullable public String declareQueue(final Queue queue) { try { @@ -364,19 +363,19 @@ public void deleteQueue(final String queueName, final boolean unused, final bool } private void removeQueueBindings(final String queueName) { - synchronized (this.manualDeclarables) { + this.manualDeclarablesLock.lock(); + try { this.manualDeclarables.stream() .filter(dec -> dec instanceof Queue queue && queue.getName().equals(queueName)) .collect(Collectors.toSet()) - .forEach(q -> this.manualDeclarables.remove(q)); - Iterator iterator = this.manualDeclarables.iterator(); - while (iterator.hasNext()) { - Declarable next = iterator.next(); - if (next instanceof Binding binding && - (binding.isDestinationQueue() && binding.getDestination().equals(queueName))) { - iterator.remove(); - } - } + .forEach(this.manualDeclarables::remove); + this.manualDeclarables.removeIf(next -> + next instanceof Binding binding + && (binding.isDestinationQueue() + && binding.getDestination().equals(queueName))); + } + finally { + this.manualDeclarablesLock.unlock(); } } @@ -405,8 +404,7 @@ public int purgeQueue(final String queueName) { // Binding @Override - @ManagedOperation(description = - "Declare a binding on the broker (this operation is not available remotely)") + @ManagedOperation(description = "Declare a binding on the broker (this operation is not available remotely)") public void declareBinding(final Binding binding) { try { this.rabbitTemplate.execute(channel -> { @@ -423,8 +421,7 @@ public void declareBinding(final Binding binding) { } @Override - @ManagedOperation(description = - "Remove a binding from the broker (this operation is not available remotely)") + @ManagedOperation(description = "Remove a binding from the broker (this operation is not available remotely)") public void removeBinding(final Binding binding) { this.rabbitTemplate.execute(channel -> { if (binding.isDestinationQueue()) { @@ -520,9 +517,9 @@ public boolean isRedeclareManualDeclarations() { /** * Normally, when a connection is recovered, the admin only recovers auto-delete - * queues, etc, that are declared as beans in the application context. When this is + * queues, etc., that are declared as beans in the application context. When this is * true, it will also redeclare any manually declared {@link Declarable}s via admin - * methods. When a queue or exhange is deleted, it will not longer be recovered, nor + * methods. When a queue or exchange is deleted, it will no longer be recovered, nor * will any corresponding bindings. * @param redeclareManualDeclarations true to redeclare. * @since 2.4 @@ -583,8 +580,8 @@ public boolean isAutoStartup() { */ @Override public void afterPropertiesSet() { - - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { if (this.running || !this.autoStartup) { return; @@ -618,7 +615,7 @@ public void afterPropertiesSet() { /* * ...but it is possible for this to happen twice in the same ConnectionFactory (if more than * one concurrent Connection is allowed). It's idempotent, so no big deal (a bit of network - * chatter). In fact it might even be a good thing: exclusive queues only make sense if they are + * chatter). In fact, it might even be a good thing: exclusive queues only make sense if they are * declared for every connection. If anyone has a problem with it: use auto-startup="false". */ if (this.retryTemplate != null) { @@ -638,7 +635,9 @@ public void afterPropertiesSet() { }); this.running = true; - + } + finally { + this.lifecycleLock.unlock(); } } @@ -657,11 +656,11 @@ public void initialize() { } this.logger.debug("Initializing declarations"); - Collection contextExchanges = new LinkedList( + Collection contextExchanges = new LinkedList<>( this.applicationContext.getBeansOfType(Exchange.class).values()); - Collection contextQueues = new LinkedList( + Collection contextQueues = new LinkedList<>( this.applicationContext.getBeansOfType(Queue.class).values()); - Collection contextBindings = new LinkedList( + Collection contextBindings = new LinkedList<>( this.applicationContext.getBeansOfType(Binding.class).values()); Collection customizers = this.applicationContext.getBeansOfType(DeclarableCustomizer.class).values(); @@ -673,7 +672,7 @@ public void initialize() { final Collection bindings = filterDeclarables(contextBindings, customizers); for (Exchange exchange : exchanges) { - if ((!exchange.isDurable() || exchange.isAutoDelete()) && this.logger.isInfoEnabled()) { + if ((!exchange.isDurable() || exchange.isAutoDelete()) && this.logger.isInfoEnabled()) { this.logger.info("Auto-declaring a non-durable or auto-delete Exchange (" + exchange.getName() + ") durable:" + exchange.isDurable() + ", auto-delete:" + exchange.isAutoDelete() + ". " @@ -693,14 +692,14 @@ public void initialize() { } } - if (exchanges.size() == 0 && queues.size() == 0 && bindings.size() == 0 && this.manualDeclarables.size() == 0) { + if (exchanges.isEmpty() && queues.isEmpty() && bindings.isEmpty() && this.manualDeclarables.isEmpty()) { this.logger.debug("Nothing to declare"); return; } this.rabbitTemplate.execute(channel -> { - declareExchanges(channel, exchanges.toArray(new Exchange[exchanges.size()])); - declareQueues(channel, queues.toArray(new Queue[queues.size()])); - declareBindings(channel, bindings.toArray(new Binding[bindings.size()])); + declareExchanges(channel, exchanges.toArray(new Exchange[0])); + declareQueues(channel, queues.toArray(new Queue[0])); + declareBindings(channel, bindings.toArray(new Binding[0])); return null; }); this.logger.debug("Declarations finished"); @@ -711,8 +710,9 @@ public void initialize() { * Process manual declarables. */ private void redeclareManualDeclarables() { - if (this.manualDeclarables.size() > 0) { - synchronized (this.manualDeclarables) { + if (!this.manualDeclarables.isEmpty()) { + this.manualDeclarablesLock.lock(); + try { this.logger.debug("Redeclaring manually declared Declarables"); for (Declarable dec : this.manualDeclarables) { if (dec instanceof Queue queue) { @@ -726,6 +726,9 @@ else if (dec instanceof Exchange exch) { } } } + finally { + this.manualDeclarablesLock.unlock(); + } } } @@ -805,7 +808,7 @@ private Collection filterDeclarables(Collection dec customizers.forEach(cust -> ref.set((T) cust.apply(ref.get()))); return ref.get(); }) - .collect(Collectors.toList()); + .toList(); } private boolean declarableByMe(T dec) { @@ -827,10 +830,10 @@ private void declareExchanges(final Channel channel, final Exchange... exchanges if (exchange.isDelayed()) { Map arguments = exchange.getArguments(); if (arguments == null) { - arguments = new HashMap(); + arguments = new HashMap<>(); } else { - arguments = new HashMap(arguments); + arguments = new HashMap<>(arguments); } arguments.put("x-delayed-type", exchange.getType()); channel.exchangeDeclare(exchange.getName(), DELAYED_MESSAGE_EXCHANGE, exchange.isDurable(), @@ -849,9 +852,8 @@ private void declareExchanges(final Channel channel, final Exchange... exchanges } private DeclareOk[] declareQueues(final Channel channel, final Queue... queues) throws IOException { - List declareOks = new ArrayList(queues.length); - for (int i = 0; i < queues.length; i++) { - Queue queue = queues[i]; + List declareOks = new ArrayList<>(queues.length); + for (Queue queue : queues) { if (!queue.getName().startsWith("amq.")) { if (this.logger.isDebugEnabled()) { this.logger.debug("declaring Queue '" + queue.getName() + "'"); @@ -878,7 +880,7 @@ else if (this.logger.isDebugEnabled()) { this.logger.debug(queue.getName() + ": Queue with name that starts with 'amq.' cannot be declared."); } } - return declareOks.toArray(new DeclareOk[declareOks.size()]); + return declareOks.toArray(new DeclareOk[0]); } private void closeChannelAfterIllegalArg(final Channel channel, Queue queue) { From 2fbe62f23d439e12be4ffcc0aac892b7ec136bf4 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Sat, 16 Dec 2023 09:37:53 -0500 Subject: [PATCH 327/737] GH-2574: RabbitTemplate: synchronized to Lock Fixes: #2574 * Rework all the `synchronized` blocks in the `RabbitTemplate` to `Lock` * Fix typos in Javadocs * Some other code reformatting suggested by IDE --- .../amqp/rabbit/core/RabbitTemplate.java | 140 ++++++++++-------- 1 file changed, 82 insertions(+), 58 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java index 0b4ebe5ff3..866116b073 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java @@ -24,6 +24,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -35,6 +36,8 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.springframework.amqp.AmqpConnectException; import org.springframework.amqp.AmqpException; @@ -187,10 +190,9 @@ public class RabbitTemplate extends RabbitAccessor // NOSONAR type line count private final AtomicInteger activeTemplateCallbacks = new AtomicInteger(); - private final ConcurrentMap publisherConfirmChannels = - new ConcurrentHashMap(); + private final ConcurrentMap publisherConfirmChannels = new ConcurrentHashMap<>(); - private final Map replyHolder = new ConcurrentHashMap(); + private final Map replyHolder = new ConcurrentHashMap<>(); private final String uuid = UUID.randomUUID().toString(); @@ -201,9 +203,13 @@ public class RabbitTemplate extends RabbitAccessor // NOSONAR type line count private final ReplyToAddressCallback defaultReplyToAddressCallback = (request, reply) -> getReplyToAddress(request); + private final Lock fastReplyToLock = new ReentrantLock(); + private final Map directReplyToContainers = new HashMap<>(); + private final Lock directReplyToContainersLock = new ReentrantLock(); + private final AtomicInteger containerInstance = new AtomicInteger(); private final Map consumerArgs = new HashMap<>(); @@ -234,7 +240,7 @@ public class RabbitTemplate extends RabbitAccessor // NOSONAR type line count private ReturnsCallback returnsCallback; - private Expression mandatoryExpression = new ValueExpression(false); + private Expression mandatoryExpression = new ValueExpression<>(false); private String correlationKey = null; @@ -294,7 +300,6 @@ public RabbitTemplate() { /** * Create a rabbit template with default strategies and settings. - * * @param connectionFactory the connection factory to use */ public RabbitTemplate(ConnectionFactory connectionFactory) { @@ -340,9 +345,8 @@ public void setObservationConvention(RabbitTemplateObservationConvention observa } /** - * The name of the default exchange to use for send operations when none is specified. Defaults to "" + * The name of the default exchange to use for send operations when none is specified. Defaults to {@code ""} * which is the default exchange in the broker (per the AMQP specification). - * * @param exchange the exchange name to use for send operations */ public void setExchange(@Nullable String exchange) { @@ -351,7 +355,6 @@ public void setExchange(@Nullable String exchange) { /** * @return the name of the default exchange used by this template. - * * @since 1.6 */ public String getExchange() { @@ -362,7 +365,6 @@ public String getExchange() { * The value of a default routing key to use for send operations when none is specified. Default is empty which is * not helpful when using the default (or any direct) exchange, but fine if the exchange is a headers exchange for * instance. - * * @param routingKey the default routing key to use for send operations */ public void setRoutingKey(String routingKey) { @@ -399,7 +401,6 @@ public String getDefaultReceiveQueue() { /** * The encoding to use when converting between byte arrays and Strings in message properties. - * * @param encoding the encoding to set */ public void setEncoding(String encoding) { @@ -416,14 +417,14 @@ public String getEncoding() { /** * An address for replies; if not provided, a temporary exclusive, auto-delete queue will - * be used for each reply, unless RabbitMQ supports 'amq.rabbitmq.reply-to' - see - * https://www.rabbitmq.com/direct-reply-to.html + * be used for each reply, unless RabbitMQ supports + * 'amq.rabbitmq.reply-to' *

The address can be a simple queue name (in which case the reply will be routed via the default * exchange), or with the form {@code exchange/routingKey} to route the reply using an explicit * exchange and routing key. * @param replyAddress the replyAddress to set */ - public synchronized void setReplyAddress(String replyAddress) { + public void setReplyAddress(String replyAddress) { this.replyAddress = replyAddress; this.evaluatedFastReplyTo = false; } @@ -445,9 +446,7 @@ public void setReceiveTimeout(long receiveTimeout) { * sendAndReceive methods. The default value is defined as {@link #DEFAULT_REPLY_TIMEOUT}. A negative value * indicates an indefinite timeout. Not used in the plain receive methods because there is no blocking receive * operation defined in the protocol. - * * @param replyTimeout the reply timeout in milliseconds - * * @see #sendAndReceive(String, String, Message) * @see #convertSendAndReceive(String, String, Object) */ @@ -461,9 +460,7 @@ public void setReplyTimeout(long replyTimeout) { *

* The default converter is a SimpleMessageConverter, which is able to handle byte arrays, Strings, and Serializable * Objects depending on the message content type header. - * * @param messageConverter The message converter. - * * @see #convertAndSend * @see #receiveAndConvert * @see org.springframework.amqp.support.converter.SimpleMessageConverter @@ -477,7 +474,6 @@ public void setMessageConverter(MessageConverter messageConverter) { * content in the message headers and plain Java objects. In particular there are limitations when dealing with very * long string headers, which hopefully are rare in practice, but if you need to use long headers you might need to * inject a special converter here. - * * @param messagePropertiesConverter The message properties converter. */ public void setMessagePropertiesConverter(MessagePropertiesConverter messagePropertiesConverter) { @@ -486,7 +482,7 @@ public void setMessagePropertiesConverter(MessagePropertiesConverter messageProp } /** - * Return the properties converter. + * Return the converter for properties. * @return the converter. * @since 2.0 */ @@ -497,7 +493,6 @@ protected MessagePropertiesConverter getMessagePropertiesConverter() { /** * Return the message converter for this template. Useful for clients that want to take advantage of the converter * in {@link ChannelCallback} implementations. - * * @return The message converter. */ public MessageConverter getMessageConverter() { @@ -765,7 +760,7 @@ public void setCorrelationDataPostProcessor(CorrelationDataPostProcessor correla /** * By default, when the broker supports it and no * {@link #setReplyAddress(String) replyAddress} is provided, send/receive - * methods will use Direct reply-to (https://www.rabbitmq.com/direct-reply-to.html). + * methods will use Direct reply-to. * Setting this property to true will override that behavior and use * a temporary, auto-delete, queue for each request instead. * Changing this property has no effect once the first request has been @@ -778,7 +773,7 @@ public void setUseTemporaryReplyQueues(boolean value) { } /** - * Set whether or not to use a {@link DirectReplyToMessageListenerContainer} when + * Set whether to use a {@link DirectReplyToMessageListenerContainer} when * direct reply-to is available and being used. When false, a new consumer is created * for each request (the mechanism used in versions prior to 2.0). Default true. * @param useDirectReplyToContainer set to false to use a consumer per request. @@ -793,7 +788,7 @@ public void setUseDirectReplyToContainer(boolean useDirectReplyToContainer) { * Set an expression to be evaluated to set the userId message property if it * evaluates to a non-null value and the property is not already set in the * message to be sent. - * See https://www.rabbitmq.com/validated-user-id.html + * See validated-user-id * @param userIdExpression the expression. * @since 1.6 */ @@ -805,7 +800,7 @@ public void setUserIdExpression(Expression userIdExpression) { * Set an expression to be evaluated to set the userId message property if it * evaluates to a non-null value and the property is not already set in the * message to be sent. - * See https://www.rabbitmq.com/validated-user-id.html + * See validated-user-id * @param userIdExpression the expression. * @since 1.6 */ @@ -941,7 +936,7 @@ public Collection getUnconfirmed(long age) { unconfirmed.add(confirm.getCorrelationData()); } } - return unconfirmed.size() > 0 ? unconfirmed : null; + return !unconfirmed.isEmpty() ? unconfirmed : null; } /** @@ -998,13 +993,17 @@ protected void doStart() { @Override public void stop() { - synchronized (this.directReplyToContainers) { + this.directReplyToContainersLock.lock(); + try { this.directReplyToContainers.values() .stream() .filter(AbstractMessageListenerContainer::isRunning) .forEach(AbstractMessageListenerContainer::stop); this.directReplyToContainers.clear(); } + finally { + this.directReplyToContainersLock.unlock(); + } doStop(); } @@ -1018,11 +1017,15 @@ protected void doStop() { @Override public boolean isRunning() { - synchronized (this.directReplyToContainers) { + this.directReplyToContainersLock.lock(); + try { return this.directReplyToContainers.values() .stream() .anyMatch(AbstractMessageListenerContainer::isRunning); } + finally { + this.directReplyToContainersLock.unlock(); + } } @Override @@ -1036,16 +1039,15 @@ private void evaluateFastReplyTo() { } /** - * Override this method use some other criteria to decide whether or not to use - * direct reply-to (https://www.rabbitmq.com/direct-reply-to.html). + * Override this method use some other criteria to decide whether to use + * (direct reply-to). * The default implementation returns true if the broker supports it and there * is no {@link #setReplyAddress(String) replyAddress} set and * {@link #setUseTemporaryReplyQueues(boolean) useTemporaryReplyQueues} is false. * When direct reply-to is not used, the template * will create a temporary, exclusive, auto-delete queue for the reply. *

- * This method is invoked once only - when the first message is sent, from a - * synchronized block. + * This method is invoked once only - when the first message is sent, from a locked block. * @return true to use direct reply-to. */ protected boolean useDirectReplyTo() { @@ -1116,6 +1118,7 @@ public void send(final String exchange, final String routingKey, final Message m public void send(final String exchange, final String routingKey, final Message message, @Nullable final CorrelationData correlationData) throws AmqpException { + execute(channel -> { doSend(channel, exchange, routingKey, message, (RabbitTemplate.this.returnsCallback != null @@ -1126,7 +1129,9 @@ && isMandatoryFor(message), }, obtainTargetConnectionFactory(this.sendConnectionFactorySelectorExpression, message)); } - private ConnectionFactory obtainTargetConnectionFactory(Expression expression, @Nullable Object rootObject) { + private ConnectionFactory obtainTargetConnectionFactory(@Nullable Expression expression, + @Nullable Object rootObject) { + if (expression != null && getConnectionFactory() instanceof AbstractRoutingConnectionFactory routingCF) { Object lookupKey; if (rootObject != null) { @@ -1167,6 +1172,7 @@ public void convertAndSend(String routingKey, final Object object) throws AmqpEx @Override public void convertAndSend(String routingKey, final Object object, CorrelationData correlationData) throws AmqpException { + convertAndSend(this.exchange, routingKey, object, correlationData); } @@ -1190,6 +1196,7 @@ public void convertAndSend(Object message, MessagePostProcessor messagePostProce @Override public void convertAndSend(String routingKey, Object message, MessagePostProcessor messagePostProcessor) throws AmqpException { + convertAndSend(this.exchange, routingKey, message, messagePostProcessor, null); } @@ -1197,6 +1204,7 @@ public void convertAndSend(String routingKey, Object message, MessagePostProcess public void convertAndSend(Object message, MessagePostProcessor messagePostProcessor, CorrelationData correlationData) throws AmqpException { + convertAndSend(this.exchange, this.routingKey, message, messagePostProcessor, correlationData); } @@ -1204,12 +1212,14 @@ public void convertAndSend(Object message, MessagePostProcessor messagePostProce public void convertAndSend(String routingKey, Object message, MessagePostProcessor messagePostProcessor, CorrelationData correlationData) throws AmqpException { + convertAndSend(this.exchange, routingKey, message, messagePostProcessor, correlationData); } @Override public void convertAndSend(String exchange, String routingKey, final Object message, final MessagePostProcessor messagePostProcessor) throws AmqpException { + convertAndSend(exchange, routingKey, message, messagePostProcessor, null); } @@ -1217,6 +1227,7 @@ public void convertAndSend(String exchange, String routingKey, final Object mess public void convertAndSend(String exchange, String routingKey, final Object message, final MessagePostProcessor messagePostProcessor, @Nullable CorrelationData correlationData) throws AmqpException { + Message messageToSend = convertMessageIfNecessary(message); messageToSend = messagePostProcessor.postProcessMessage(messageToSend, correlationData, nullSafeExchange(exchange), nullSafeRoutingKey(routingKey)); @@ -1226,7 +1237,7 @@ public void convertAndSend(String exchange, String routingKey, final Object mess @Override @Nullable public Message receive() throws AmqpException { - return this.receive(getRequiredQueue()); + return receive(getRequiredQueue()); } @Override @@ -1258,7 +1269,7 @@ protected Message doReceiveNoWait(final String queueName) { channel.txCommit(); } else if (isChannelTransacted()) { - // Not locally transacted but it is transacted so it + // Not locally transacted, but it is transacted, so it // could be synchronized with an external transaction ConnectionFactoryUtils.registerDeliveryTag(getConnectionFactory(), channel, deliveryTag); } @@ -1313,7 +1324,7 @@ else if (isChannelTransacted()) { @Override @Nullable public Object receiveAndConvert() throws AmqpException { - return receiveAndConvert(this.getRequiredQueue()); + return receiveAndConvert(getRequiredQueue()); } @Override @@ -1325,7 +1336,7 @@ public Object receiveAndConvert(String queueName) throws AmqpException { @Override @Nullable public Object receiveAndConvert(long timeoutMillis) throws AmqpException { - return receiveAndConvert(this.getRequiredQueue(), timeoutMillis); + return receiveAndConvert(getRequiredQueue(), timeoutMillis); } @Override @@ -1341,7 +1352,7 @@ public Object receiveAndConvert(String queueName, long timeoutMillis) throws Amq @Override @Nullable public T receiveAndConvert(ParameterizedTypeReference type) throws AmqpException { - return receiveAndConvert(this.getRequiredQueue(), type); + return receiveAndConvert(getRequiredQueue(), type); } @Override @@ -1353,13 +1364,15 @@ public T receiveAndConvert(String queueName, ParameterizedTypeReference t @Override @Nullable public T receiveAndConvert(long timeoutMillis, ParameterizedTypeReference type) throws AmqpException { - return receiveAndConvert(this.getRequiredQueue(), timeoutMillis, type); + return receiveAndConvert(getRequiredQueue(), timeoutMillis, type); } @Override @SuppressWarnings(UNCHECKED) @Nullable - public T receiveAndConvert(String queueName, long timeoutMillis, ParameterizedTypeReference type) throws AmqpException { + public T receiveAndConvert(String queueName, long timeoutMillis, ParameterizedTypeReference type) + throws AmqpException { + Message response = timeoutMillis == 0 ? doReceiveNoWait(queueName) : receive(queueName, timeoutMillis); if (response != null) { return (T) getRequiredSmartMessageConverter().fromMessage(response, type); @@ -1374,7 +1387,9 @@ public boolean receiveAndReply(ReceiveAndReplyCallback callback) th @Override @SuppressWarnings(UNCHECKED) - public boolean receiveAndReply(final String queueName, ReceiveAndReplyCallback callback) throws AmqpException { + public boolean receiveAndReply(final String queueName, ReceiveAndReplyCallback callback) + throws AmqpException { + return receiveAndReply(queueName, callback, (ReplyToAddressCallback) this.defaultReplyToAddressCallback); } @@ -1404,6 +1419,7 @@ public boolean receiveAndReply(ReceiveAndReplyCallback callback, @Override public boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback, ReplyToAddressCallback replyToAddressCallback) throws AmqpException { + return doReceiveAndReply(queueName, callback, replyToAddressCallback); } @@ -1417,7 +1433,7 @@ private boolean doReceiveAndReply(final String queueName, final ReceiveAn } return false; }, obtainTargetConnectionFactory(this.receiveConnectionFactorySelectorExpression, queueName)); - return result == null ? false : result; + return result != null && result; } @Nullable @@ -1435,7 +1451,7 @@ private Message receiveForReply(final String queueName, Channel channel) throws channel.basicAck(deliveryTag1, false); } else if (channelTransacted) { - // Not locally transacted but it is transacted so it could be + // Not locally transacted, but it is transacted, so it could be // synchronized with an external transaction ConnectionFactoryUtils.registerDeliveryTag(getConnectionFactory(), channel, deliveryTag1); } @@ -1447,7 +1463,7 @@ else if (channelTransacted) { if (delivery != null) { long deliveryTag2 = delivery.getEnvelope().getDeliveryTag(); if (channelTransacted && !channelLocallyTransacted) { - // Not locally transacted but it is transacted so it could be + // Not locally transacted, but it is transacted, so it could be // synchronized with an external transaction ConnectionFactoryUtils.registerDeliveryTag(getConnectionFactory(), channel, deliveryTag2); } @@ -1521,12 +1537,7 @@ private Delivery consumeDelivery(Channel channel, String queueName, long timeout */ protected void logReceived(String prefix, @Nullable Message message) { if (logger.isDebugEnabled()) { - if (message == null) { - logger.debug(prefix + "no message"); - } - else { - logger.debug(prefix + message); - } + logger.debug(prefix + Objects.requireNonNullElse(message, "no message")); } } @@ -1546,7 +1557,9 @@ private boolean sendReply(final ReceiveAndReplyCallback callback, } catch (ClassCastException e) { StackTraceElement[] trace = e.getStackTrace(); - if (trace[0].getMethodName().equals("handle") && trace[1].getFileName().equals("RabbitTemplate.java")) { + if (trace[0].getMethodName().equals("handle") + && Objects.equals(trace[1].getFileName(), "RabbitTemplate.java")) { + throw new IllegalArgumentException("ReceiveAndReplyCallback '" + callback + "' can't handle received object '" + receive + "'", e); } @@ -1566,7 +1579,7 @@ else if (isChannelLocallyTransacted(channel)) { } private void doSendReply(final ReplyToAddressCallback replyToAddressCallback, Channel channel, - Message receiveMessage, S reply) throws IOException { + Message receiveMessage, S reply) { Address replyTo = replyToAddressCallback.getReplyToAddress(receiveMessage, reply); @@ -1691,6 +1704,7 @@ public Object convertSendAndReceive(final String exchange, final String routingK @Nullable public Object convertSendAndReceive(final Object message, final MessagePostProcessor messagePostProcessor) throws AmqpException { + return convertSendAndReceive(message, messagePostProcessor, null); } @@ -1723,6 +1737,7 @@ public Object convertSendAndReceive(final String routingKey, final Object messag @Nullable public Object convertSendAndReceive(final String exchange, final String routingKey, final Object message, final MessagePostProcessor messagePostProcessor) throws AmqpException { + return convertSendAndReceive(exchange, routingKey, message, messagePostProcessor, null); } @@ -1744,6 +1759,7 @@ public Object convertSendAndReceive(final String exchange, final String routingK @Nullable public T convertSendAndReceiveAsType(final Object message, ParameterizedTypeReference responseType) throws AmqpException { + return convertSendAndReceiveAsType(message, (CorrelationData) null, responseType); } @@ -1890,11 +1906,15 @@ protected Message doSendAndReceive(final String exchange, final String routingKe @Nullable CorrelationData correlationData) { if (!this.evaluatedFastReplyTo) { - synchronized (this) { + this.fastReplyToLock.lock(); + try { if (!this.evaluatedFastReplyTo) { evaluateFastReplyTo(); } } + finally { + this.fastReplyToLock.unlock(); + } } if (this.usingFastReplyTo && this.useDirectReplyToContainer) { @@ -1959,7 +1979,7 @@ public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProp }; channel.addShutdownListener(shutdownListener); channel.basicConsume(replyTo, true, consumerTag, this.noLocalReplyConsumer, true, null, consumer); - Message reply = null; + Message reply; try { reply = exchangeMessages(exchange, routingKey, message, correlationData, channel, pendingReply, messageTag); @@ -2035,7 +2055,8 @@ private Message doSendAndReceiveWithDirect(String exchange, String routingKey, M private DirectReplyToMessageListenerContainer createReplyToContainer(ConnectionFactory connectionFactory) { DirectReplyToMessageListenerContainer container; - synchronized (this.directReplyToContainers) { + this.directReplyToContainersLock.lock(); + try { container = this.directReplyToContainers.get(connectionFactory); if (container == null) { container = new DirectReplyToMessageListenerContainer(connectionFactory); @@ -2053,6 +2074,9 @@ private DirectReplyToMessageListenerContainer createReplyToContainer(ConnectionF this.replyAddress = Address.AMQ_RABBITMQ_REPLY_TO; } } + finally { + this.directReplyToContainersLock.unlock(); + } return container; } @@ -2136,7 +2160,7 @@ private void saveAndSetProperties(final Message message, final PendingReply pend @Nullable private Message exchangeMessages(final String exchange, final String routingKey, final Message message, @Nullable final CorrelationData correlationData, Channel channel, final PendingReply pendingReply, - String messageTag) throws IOException, InterruptedException { + String messageTag) throws InterruptedException { Message reply; boolean mandatory = isMandatoryFor(message); @@ -2342,6 +2366,7 @@ public T invoke(OperationsCallback action, @Nullable com.rabbitmq.client. @Nullable private ConfirmListener addConfirmListener(@Nullable com.rabbitmq.client.ConfirmCallback acks, @Nullable com.rabbitmq.client.ConfirmCallback nacks, Channel channel) { + ConfirmListener listener = null; if (acks != null && nacks != null && channel instanceof ChannelProxy proxy && proxy.isConfirmSelected()) { @@ -2412,7 +2437,6 @@ private boolean isPublisherConfirmsOrReturns(ConnectionFactory connectionFactory * @param message The Message to send. * @param mandatory The mandatory flag. * @param correlationData The correlation data. - * @throws IOException If thrown by RabbitMQ API methods. */ public void doSend(Channel channel, String exchangeArg, String routingKeyArg, Message message, boolean mandatory, @Nullable CorrelationData correlationData) { @@ -2473,7 +2497,7 @@ protected void observeTheSend(Channel channel, Message message, boolean mandator * @return the result. * @since 2.3.4 */ - public String nullSafeExchange(String exchange) { + public String nullSafeExchange(@Nullable String exchange) { return exchange == null ? this.exchange : exchange; } @@ -2483,7 +2507,7 @@ public String nullSafeExchange(String exchange) { * @return the result. * @since 2.3.4 */ - public String nullSafeRoutingKey(String rk) { + public String nullSafeRoutingKey(@Nullable String rk) { return rk == null ? this.routingKey : rk; } From f4cb1a005280b546d885287b1f95d3759d4ec4d3 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Sat, 16 Dec 2023 09:50:06 -0500 Subject: [PATCH 328/737] GH-2576: BlockQueueConsumer: synchronized to Lock Fixes: #2576 * Rework all the `synchronized` blocks in the `BlockingQueueConsumer` to `Lock` * Fix typos in Javadocs * Some other code reformatting suggested by IDE --- .../listener/BlockingQueueConsumer.java | 79 ++++++++++++------- 1 file changed, 49 insertions(+), 30 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java index a23e77e67f..f6e64c5791 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java @@ -20,13 +20,12 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.OptionalLong; import java.util.Set; import java.util.concurrent.BlockingQueue; @@ -36,6 +35,8 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import org.apache.commons.logging.Log; @@ -66,6 +67,7 @@ import org.springframework.lang.Nullable; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.backoff.BackOffExecution; @@ -96,7 +98,9 @@ public class BlockingQueueConsumer { private static final int DEFAULT_RETRY_DECLARATION_INTERVAL = 60000; - private static Log logger = LogFactory.getLog(BlockingQueueConsumer.class); + private static final Log logger = LogFactory.getLog(BlockingQueueConsumer.class); + + private final Lock lifecycleLock = new ReentrantLock(); private final BlockingQueue queue; @@ -129,17 +133,19 @@ public class BlockingQueueConsumer { private final ActiveObjectCounter activeObjectCounter; - private final Map consumerArgs = new HashMap(); + private final Map consumerArgs = new HashMap<>(); private final boolean noLocal; private final boolean exclusive; - private final Set deliveryTags = new LinkedHashSet(); + private final Set deliveryTags = new LinkedHashSet<>(); private final boolean defaultRequeueRejected; - private final Set missingQueues = Collections.synchronizedSet(new HashSet()); + private final Set missingQueues = ConcurrentHashMap.newKeySet(); + + private final Lock missingQueuesLock = new ReentrantLock(); private long retryDeclarationInterval = DEFAULT_RETRY_DECLARATION_INTERVAL; @@ -214,6 +220,7 @@ public BlockingQueueConsumer(ConnectionFactory connectionFactory, MessagePropertiesConverter messagePropertiesConverter, ActiveObjectCounter activeObjectCounter, AcknowledgeMode acknowledgeMode, boolean transactional, int prefetchCount, boolean defaultRequeueRejected, String... queues) { + this(connectionFactory, messagePropertiesConverter, activeObjectCounter, acknowledgeMode, transactional, prefetchCount, defaultRequeueRejected, null, queues); } @@ -237,6 +244,7 @@ public BlockingQueueConsumer(ConnectionFactory connectionFactory, ActiveObjectCounter activeObjectCounter, AcknowledgeMode acknowledgeMode, boolean transactional, int prefetchCount, boolean defaultRequeueRejected, @Nullable Map consumerArgs, String... queues) { + this(connectionFactory, messagePropertiesConverter, activeObjectCounter, acknowledgeMode, transactional, prefetchCount, defaultRequeueRejected, consumerArgs, false, queues); } @@ -261,6 +269,7 @@ public BlockingQueueConsumer(ConnectionFactory connectionFactory, ActiveObjectCounter activeObjectCounter, AcknowledgeMode acknowledgeMode, boolean transactional, int prefetchCount, boolean defaultRequeueRejected, @Nullable Map consumerArgs, boolean exclusive, String... queues) { + this(connectionFactory, messagePropertiesConverter, activeObjectCounter, acknowledgeMode, transactional, prefetchCount, defaultRequeueRejected, consumerArgs, false, exclusive, queues); } @@ -287,6 +296,7 @@ public BlockingQueueConsumer(ConnectionFactory connectionFactory, ActiveObjectCounter activeObjectCounter, AcknowledgeMode acknowledgeMode, boolean transactional, int prefetchCount, boolean defaultRequeueRejected, @Nullable Map consumerArgs, boolean noLocal, boolean exclusive, String... queues) { + this.connectionFactory = connectionFactory; this.messagePropertiesConverter = messagePropertiesConverter; this.activeObjectCounter = activeObjectCounter; @@ -294,13 +304,13 @@ public BlockingQueueConsumer(ConnectionFactory connectionFactory, this.transactional = transactional; this.prefetchCount = prefetchCount; this.defaultRequeueRejected = defaultRequeueRejected; - if (consumerArgs != null && consumerArgs.size() > 0) { + if (!CollectionUtils.isEmpty(consumerArgs)) { this.consumerArgs.putAll(consumerArgs); } this.noLocal = noLocal; this.exclusive = exclusive; this.queues = Arrays.copyOf(queues, queues.length); - this.queue = new LinkedBlockingQueue(queues.length == 0 ? prefetchCount : prefetchCount * queues.length); + this.queue = new LinkedBlockingQueue<>(queues.length == 0 ? prefetchCount : prefetchCount * queues.length); } public Channel getChannel() { @@ -309,8 +319,8 @@ public Channel getChannel() { public Collection getConsumerTags() { return this.consumers.values().stream() - .map(c -> c.getConsumerTag()) - .filter(tag -> tag != null) + .map(DefaultConsumer::getConsumerTag) + .filter(Objects::nonNull) .collect(Collectors.toList()); } @@ -489,7 +499,6 @@ private void checkShutdown() { * shutdown. If delivery is null, we may be in shutdown mode. Check and see. * @param delivery the delivered message contents. * @return A message built from the contents. - * @throws InterruptedException if the thread is interrupted. */ @Nullable private Message handle(@Nullable Delivery delivery) { @@ -545,7 +554,7 @@ public Message nextMessage(long timeout) throws InterruptedException, ShutdownSi logger.trace("Retrieving delivery for " + this); } checkShutdown(); - if (this.missingQueues.size() > 0) { + if (!this.missingQueues.isEmpty()) { checkMissingQueues(); } Message message = handle(this.queue.poll(timeout, TimeUnit.MILLISECONDS)); @@ -562,7 +571,8 @@ public Message nextMessage(long timeout) throws InterruptedException, ShutdownSi private void checkMissingQueues() { long now = System.currentTimeMillis(); if (now - this.retryDeclarationInterval > this.lastRetryDeclaration) { - synchronized (this.missingQueues) { + this.missingQueuesLock.lock(); + try { Iterator iterator = this.missingQueues.iterator(); while (iterator.hasNext()) { boolean available = true; @@ -598,6 +608,9 @@ private void checkMissingQueues() { } } } + finally { + this.missingQueuesLock.unlock(); + } this.lastRetryDeclaration = now; } } @@ -767,24 +780,30 @@ private void attemptPassiveDeclarations() { } } - public synchronized void stop() { - if (this.abortStarted == 0) { // signal handle delivery to use offer - this.abortStarted = System.currentTimeMillis(); - } - if (!this.cancelled()) { - try { - RabbitUtils.closeMessageConsumer(this.channel, getConsumerTags(), this.transactional); + public void stop() { + this.lifecycleLock.lock(); + try { + if (this.abortStarted == 0) { // signal handle delivery to use offer + this.abortStarted = System.currentTimeMillis(); } - catch (Exception e) { - if (logger.isDebugEnabled()) { - logger.debug("Error closing consumer " + this, e); + if (!this.cancelled()) { + try { + RabbitUtils.closeMessageConsumer(this.channel, getConsumerTags(), this.transactional); + } + catch (Exception e) { + if (logger.isDebugEnabled()) { + logger.debug("Error closing consumer " + this, e); + } } } + if (logger.isDebugEnabled()) { + logger.debug("Closing Rabbit Channel: " + this.channel); + } + forceCloseAndClearQueue(); } - if (logger.isDebugEnabled()) { - logger.debug("Closing Rabbit Channel: " + this.channel); + finally { + this.lifecycleLock.unlock(); } - forceCloseAndClearQueue(); } public void forceCloseAndClearQueue() { @@ -859,9 +878,8 @@ public void rollbackOnExceptionIfNecessary(Throwable ex, long tag) { * Perform a commit or message acknowledgement, as appropriate. * @param localTx Whether the channel is locally transacted. * @return true if at least one delivery tag exists. - * @throws IOException Any IOException. */ - public boolean commitIfNecessary(boolean localTx) throws IOException { + public boolean commitIfNecessary(boolean localTx) { if (this.deliveryTags.isEmpty()) { return false; } @@ -994,6 +1012,7 @@ public void handleCancelOk(String consumerTag) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) { + if (logger.isDebugEnabled()) { logger.debug("Storing delivery for consumerTag: '" + consumerTag + "' with deliveryTag: '" + envelope.getDeliveryTag() + "' in " @@ -1053,7 +1072,7 @@ private DeclarationException(Throwable t) { super("Failed to declare queue(s):", t); } - private final List failedQueues = new ArrayList(); + private final List failedQueues = new ArrayList<>(); private void addFailedQueue(String queue) { this.failedQueues.add(queue); @@ -1065,7 +1084,7 @@ private List getFailedQueues() { @Override public String getMessage() { - return super.getMessage() + this.failedQueues.toString(); + return super.getMessage() + this.failedQueues; } } From 243ae93668d9abb526037f1f756786030f97dd8c Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Sat, 16 Dec 2023 10:21:27 -0500 Subject: [PATCH 329/737] GH-2577: No synchronized in spring-rabbit-test Fixes: #2577 * Use `ConcurrentHashMap.newKeySet()` for `exceptions` property in `Answer` impls --- .../amqp/rabbit/test/mockito/LambdaAnswer.java | 12 ++++++------ .../LatchCountDownAndCallRealMethodAnswer.java | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LambdaAnswer.java b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LambdaAnswer.java index 33dbfee5e4..c67fe71c96 100644 --- a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LambdaAnswer.java +++ b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LambdaAnswer.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 the original author or authors. + * Copyright 2016-2023 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. @@ -17,9 +17,9 @@ package org.springframework.amqp.rabbit.test.mockito; import java.util.Collection; -import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import org.mockito.internal.stubbing.defaultanswers.ForwardsInvocations; import org.mockito.invocation.InvocationOnMock; @@ -33,6 +33,8 @@ * @param the return type. * * @author Gary Russell + * @author Artem Bilan + * * @since 1.6 * */ @@ -43,7 +45,7 @@ public class LambdaAnswer extends ForwardsInvocations { private final ValueToReturn callback; - private final Set exceptions = Collections.synchronizedSet(new LinkedHashSet<>()); + private final Set exceptions = ConcurrentHashMap.newKeySet(); private final boolean hasDelegate; @@ -88,9 +90,7 @@ public T answer(InvocationOnMock invocation) throws Throwable { * @since 2.2.3 */ public Collection getExceptions() { - synchronized (this.exceptions) { - return new LinkedHashSet<>(this.exceptions); - } + return new LinkedHashSet<>(this.exceptions); } @FunctionalInterface diff --git a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LatchCountDownAndCallRealMethodAnswer.java b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LatchCountDownAndCallRealMethodAnswer.java index d455693706..08b65fafc6 100644 --- a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LatchCountDownAndCallRealMethodAnswer.java +++ b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LatchCountDownAndCallRealMethodAnswer.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 the original author or authors. + * Copyright 2016-2023 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. @@ -17,9 +17,9 @@ package org.springframework.amqp.rabbit.test.mockito; import java.util.Collection; -import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -33,6 +33,8 @@ * method and counts down a latch. Captures any exceptions thrown. * * @author Gary Russell + * @author Artem Bilan + * * @since 1.6 * */ @@ -42,7 +44,7 @@ public class LatchCountDownAndCallRealMethodAnswer extends ForwardsInvocations { private final transient CountDownLatch latch; - private final Set exceptions = Collections.synchronizedSet(new LinkedHashSet<>()); + private final Set exceptions = ConcurrentHashMap.newKeySet(); private final boolean hasDelegate; @@ -101,9 +103,7 @@ public CountDownLatch getLatch() { */ @Nullable public Collection getExceptions() { - synchronized (this.exceptions) { - return new LinkedHashSet<>(this.exceptions); - } + return new LinkedHashSet<>(this.exceptions); } } From b58a871daa4e4281ad805502ac7fe96c31a87119 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Sat, 16 Dec 2023 18:52:20 -0500 Subject: [PATCH 330/737] GH-2577: No synchronized in the container impls Fixes: #2578 * Fix typos in Javadocs * Some other cleanups according to IDE suggestions * Upgrade to Kotlin `1.9.20` --- build.gradle | 8 +- .../AbstractMessageListenerContainer.java | 173 ++++++++++-------- .../listener/BlockingQueueConsumer.java | 2 +- .../DirectMessageListenerContainer.java | 48 +++-- .../RabbitListenerEndpointRegistrar.java | 43 +++-- .../RabbitListenerEndpointRegistry.java | 28 ++- .../SimpleMessageListenerContainer.java | 6 +- 7 files changed, 177 insertions(+), 131 deletions(-) diff --git a/build.gradle b/build.gradle index 22d021a2ca..b9c704d91a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlinVersion = '1.9.10' + ext.kotlinVersion = '1.9.20' ext.isCI = System.getenv('GITHUB_ACTION') repositories { mavenCentral() @@ -474,9 +474,9 @@ project('spring-rabbit') { } - compileTestKotlin { - kotlinOptions { - jvmTarget = '17' + compileKotlin { + compilerOptions { + allWarningsAsErrors = true } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index 00717550b3..a2b698e848 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -129,7 +129,7 @@ public abstract class AbstractMessageListenerContainer extends ObservableListene public static final long DEFAULT_SHUTDOWN_TIMEOUT = 5000; - private final Object lifecycleMonitor = new Object(); + protected final Lock lifecycleLock = new ReentrantLock(); private final ContainerDelegate delegate = this::actualInvokeListener; @@ -180,9 +180,9 @@ public abstract class AbstractMessageListenerContainer extends ObservableListene private int phase = Integer.MAX_VALUE; - private boolean active = false; + private volatile boolean active = false; - private boolean running = false; + private volatile boolean running = false; private ErrorHandler errorHandler = new ConditionalRejectingErrorHandler(); @@ -341,7 +341,7 @@ protected Map getQueueNamesToQueues() { private List queuesToNames() { return this.queues.stream() .map(Queue::getActualName) - .collect(Collectors.toList()); + .toList(); } /** @@ -379,7 +379,7 @@ public void addQueues(Queue... queues) { public boolean removeQueueNames(String... queueNames) { Assert.notNull(queueNames, "'queueNames' cannot be null"); Assert.noNullElements(queueNames, "'queueNames' cannot contain null elements"); - if (this.queues.size() > 0) { + if (!this.queues.isEmpty()) { Set toRemove = new HashSet<>(Arrays.asList(queueNames)); return this.queues.removeIf( q -> toRemove.contains(q.getActualName())); @@ -453,19 +453,19 @@ protected void checkMessageListener(Object listener) { } } - @Override - @Nullable /** * Get a reference to the message listener. * @return the message listener. */ + @Override + @Nullable public MessageListener getMessageListener() { return this.messageListener; } /** * Set an ErrorHandler to be invoked in case of any uncaught exceptions thrown while processing a Message. By - * default a {@link ConditionalRejectingErrorHandler} with its default list of fatal exceptions will be used. + * default, a {@link ConditionalRejectingErrorHandler} with its default list of fatal exceptions will be used. * * @param errorHandler The error handler. */ @@ -474,7 +474,7 @@ public void setErrorHandler(ErrorHandler errorHandler) { } /** - * Determine whether or not the container should de-batch batched + * Determine whether the container should de-batch batched * messages (true) or call the listener with the batch (false). Default: true. * @param deBatchingEnabled the deBatchingEnabled to set. * @see #setBatchingStrategy(BatchingStrategy) @@ -556,7 +556,6 @@ public boolean removeAfterReceivePostProcessor(MessagePostProcessor afterReceive * Set whether to automatically start the container after initialization. *

* Default is "true"; set this to "false" to allow for manual startup through the {@link #start()} method. - * * @param autoStartup true for auto startup. */ @Override @@ -573,7 +572,6 @@ public boolean isAutoStartup() { * Specify the phase in which this container should be started and stopped. The startup order proceeds from lowest * to highest, and the shutdown order is the reverse of that. By default this value is Integer.MAX_VALUE meaning * that this container starts as late as possible and stops as soon as possible. - * * @param phase The phase. */ public void setPhase(int phase) { @@ -925,7 +923,7 @@ protected AmqpAdmin getAmqpAdmin() { /** * Set the {@link AmqpAdmin}, used to declare any auto-delete queues, bindings - * etc when the container is started. Only needed if those queues use conditional + * etc. when the container is started. Only needed if those queues use conditional * declaration (have a 'declared-by' attribute). If not specified, an internal * admin will be used which will attempt to declare all elements not having a * 'declared-by' attribute. @@ -937,7 +935,7 @@ public void setAmqpAdmin(AmqpAdmin amqpAdmin) { } /** - * If all of the configured queue(s) are not available on the broker, this setting + * If all the configured queue(s) are not available on the broker, this setting * determines whether the condition is fatal. When true, and * the queues are missing during startup, the context refresh() will fail. *

When false, the condition is not considered fatal and the container will @@ -961,7 +959,7 @@ protected boolean isMissingQueuesFatalSet() { /** * Prevent the container from starting if any of the queues defined in the context have - * mismatched arguments (TTL etc). Default false. + * mismatched arguments (TTL etc.). Default false. * @param mismatchedQueuesFatal true to fail initialization when this condition occurs. * @since 1.6 */ @@ -1256,32 +1254,37 @@ public void destroy() { * Creates a Rabbit Connection and calls {@link #doInitialize()}. */ public void initialize() { - try { - synchronized (this.lifecycleMonitor) { - this.lifecycleMonitor.notifyAll(); - } - initializeProxy(this.delegate); - checkMissingQueuesFatalFromProperty(); - checkPossibleAuthenticationFailureFatalFromProperty(); - doInitialize(); - if (!this.isExposeListenerChannel() && this.transactionManager != null) { - logger.warn("exposeListenerChannel=false is ignored when using a TransactionManager"); - } - if (!this.taskExecutorSet && StringUtils.hasText(getListenerId())) { - this.taskExecutor = new SimpleAsyncTaskExecutor(getListenerId() + "-"); - this.taskExecutorSet = true; + if (!this.initialized) { + this.lifecycleLock.lock(); + try { + if (!this.initialized) { + initializeProxy(this.delegate); + checkMissingQueuesFatalFromProperty(); + checkPossibleAuthenticationFailureFatalFromProperty(); + doInitialize(); + if (!this.isExposeListenerChannel() && this.transactionManager != null) { + logger.warn("exposeListenerChannel=false is ignored when using a TransactionManager"); + } + if (!this.taskExecutorSet && StringUtils.hasText(getListenerId())) { + this.taskExecutor = new SimpleAsyncTaskExecutor(getListenerId() + "-"); + this.taskExecutorSet = true; + } + if (this.transactionManager != null && !isChannelTransacted()) { + logger.debug("The 'channelTransacted' is coerced to 'true', when 'transactionManager' is provided"); + setChannelTransacted(true); + } + if (this.messageListener != null) { + this.messageListener.containerAckMode(this.acknowledgeMode); + } + this.initialized = true; + } } - if (this.transactionManager != null && !isChannelTransacted()) { - logger.debug("The 'channelTransacted' is coerced to 'true', when 'transactionManager' is provided"); - setChannelTransacted(true); + catch (Exception ex) { + throw convertRabbitAccessException(ex); } - if (this.messageListener != null) { - this.messageListener.containerAckMode(this.acknowledgeMode); + finally { + this.lifecycleLock.unlock(); } - this.initialized = true; - } - catch (Exception ex) { - throw convertRabbitAccessException(ex); } } @@ -1291,6 +1294,7 @@ public void initialize() { */ public void shutdown() { shutdown(null); + this.initialized = false; } /** @@ -1299,17 +1303,19 @@ public void shutdown() { * @param callback an optional {@link Runnable} to call when the stop is complete. */ public void shutdown(@Nullable Runnable callback) { - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { if (!isActive()) { logger.debug("Shutdown ignored - container is not active already"); - this.lifecycleMonitor.notifyAll(); if (callback != null) { callback.run(); } return; } this.active = false; - this.lifecycleMonitor.notifyAll(); + } + finally { + this.lifecycleLock.unlock(); } logger.debug("Shutting down Rabbit listener container"); @@ -1327,9 +1333,12 @@ public void shutdown(@Nullable Runnable callback) { } protected void setNotRunning() { - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { this.running = false; - this.lifecycleMonitor.notifyAll(); + } + finally { + this.lifecycleLock.unlock(); } } @@ -1365,9 +1374,7 @@ protected void shutdownAndWaitOrCallback(@Nullable Runnable callback) { * @return Whether this container is currently active, that is, whether it has been set up but not shut down yet. */ public final boolean isActive() { - synchronized (this.lifecycleMonitor) { - return this.active; - } + return this.active; } /** @@ -1380,11 +1387,15 @@ public void start() { return; } if (!this.initialized) { - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { if (!this.initialized) { afterPropertiesSet(); } } + finally { + this.lifecycleLock.unlock(); + } } checkObservation(); try { @@ -1406,10 +1417,13 @@ public void start() { */ protected void doStart() { // Reschedule paused tasks, if any. - synchronized (this.lifecycleMonitor) { + this.lifecycleLock.lock(); + try { this.active = true; this.running = true; - this.lifecycleMonitor.notifyAll(); + } + finally { + this.lifecycleLock.unlock(); } } @@ -1445,9 +1459,7 @@ protected void doStop() { */ @Override public final boolean isRunning() { - synchronized (this.lifecycleMonitor) { - return (this.running); - } + return this.running; } /** @@ -1596,8 +1608,7 @@ else if (listener instanceof MessageListener msgListener) { // NOSONAR if (bindChannel) { RabbitResourceHolder resourceHolder = new RabbitResourceHolder(channel, false); resourceHolder.setSynchronizedWithTransaction(true); - TransactionSynchronizationManager.bindResource(this.getConnectionFactory(), - resourceHolder); + TransactionSynchronizationManager.bindResource(getConnectionFactory(), resourceHolder); } try { doInvokeListener(msgListener, data); @@ -1605,7 +1616,7 @@ else if (listener instanceof MessageListener msgListener) { // NOSONAR finally { if (bindChannel) { // unbind if we bound - TransactionSynchronizationManager.unbindResource(this.getConnectionFactory()); + TransactionSynchronizationManager.unbindResource(getConnectionFactory()); } } } @@ -1643,13 +1654,12 @@ protected void doInvokeListener(ChannelAwareMessageListener listener, Channel ch /* * If there is a real transaction, the resource will have been bound; otherwise * we need to bind it temporarily here. Any work done on this channel - * will be committed in the finally block. + * will be committed in the {@code finally} block. */ if (isChannelLocallyTransacted() && !TransactionSynchronizationManager.isActualTransactionActive()) { resourceHolder.setSynchronizedWithTransaction(true); - TransactionSynchronizationManager.bindResource(this.getConnectionFactory(), - resourceHolder); + TransactionSynchronizationManager.bindResource(getConnectionFactory(), resourceHolder); boundHere = true; } } @@ -1658,8 +1668,7 @@ protected void doInvokeListener(ChannelAwareMessageListener listener, Channel ch if (isChannelLocallyTransacted()) { RabbitResourceHolder localResourceHolder = new RabbitResourceHolder(channelToUse, false); localResourceHolder.setSynchronizedWithTransaction(true); - TransactionSynchronizationManager.bindResource(this.getConnectionFactory(), - localResourceHolder); + TransactionSynchronizationManager.bindResource(getConnectionFactory(), localResourceHolder); boundHere = true; } } @@ -1711,10 +1720,8 @@ private void cleanUpAfterInvoke(@Nullable RabbitResourceHolder resourceHolder, C * Default implementation performs a plain invocation of the onMessage method. *

* Exception thrown from listener will be wrapped to {@link ListenerExecutionFailedException}. - * * @param listener the Rabbit MessageListener to invoke * @param data the received Rabbit Message or List of Message. - * * @see org.springframework.amqp.core.MessageListener#onMessage */ @SuppressWarnings(UNCHECKED) @@ -1899,7 +1906,7 @@ else if (this.missingQueuesFatal) { * Declaration is idempotent so, aside from some network chatter, there is no issue, * and we only will do it if we detect our queue is gone. *

- * In general it makes sense only for the 'auto-delete' or 'expired' queues, + * In general, it makes sense only for the 'auto-delete' or 'expired' queues, * but with the server TTL policy we don't have ability to determine 'expiration' * option for the queue. *

@@ -1908,25 +1915,31 @@ else if (this.missingQueuesFatal) { * the declarations are always attempted during restart so the listener will * fail with a fatal error if mismatches occur. */ - protected synchronized void redeclareElementsIfNecessary() { - AmqpAdmin admin = getAmqpAdmin(); - if (!this.lazyLoad && admin != null && isAutoDeclare()) { - try { - attemptDeclarations(admin); - this.logDeclarationException.set(true); - } - catch (Exception e) { - if (RabbitUtils.isMismatchedQueueArgs(e)) { - throw new FatalListenerStartupException("Mismatched queues", e); - } - if (this.logDeclarationException.getAndSet(false)) { - this.logger.error("Failed to check/redeclare auto-delete queue(s).", e); + protected void redeclareElementsIfNecessary() { + this.lifecycleLock.lock(); + try { + AmqpAdmin admin = getAmqpAdmin(); + if (!this.lazyLoad && admin != null && isAutoDeclare()) { + try { + attemptDeclarations(admin); + this.logDeclarationException.set(true); } - else { - this.logger.error("Failed to check/redeclare auto-delete queue(s)."); + catch (Exception e) { + if (RabbitUtils.isMismatchedQueueArgs(e)) { + throw new FatalListenerStartupException("Mismatched queues", e); + } + if (this.logDeclarationException.getAndSet(false)) { + this.logger.error("Failed to check/redeclare auto-delete queue(s).", e); + } + else { + this.logger.error("Failed to check/redeclare auto-delete queue(s)."); + } } } } + finally { + this.lifecycleLock.unlock(); + } } private void attemptDeclarations(AmqpAdmin admin) { @@ -1987,7 +2000,7 @@ else if (cause instanceof AmqpRejectAndDontRequeueException || cause instanceof * @param resourceHolder the bound resource holder (if a transaction is active). * @param exception the exception. */ - protected void prepareHolderForRollback(RabbitResourceHolder resourceHolder, RuntimeException exception) { + protected void prepareHolderForRollback(@Nullable RabbitResourceHolder resourceHolder, RuntimeException exception) { if (resourceHolder != null) { resourceHolder.setRequeueOnRollback(isAlwaysRequeueWithTxManagerRollback() || ContainerUtils.shouldRequeue(isDefaultRequeueRejected(), exception, logger)); @@ -2042,7 +2055,7 @@ protected List debatch(Message message) { if (this.isBatchListener && isDeBatchingEnabled() && getBatchingStrategy().canDebatch(message.getMessageProperties())) { final List messageList = new ArrayList<>(); - getBatchingStrategy().deBatch(message, fragment -> messageList.add(fragment)); + getBatchingStrategy().deBatch(message, messageList::add); return messageList; } return null; @@ -2110,7 +2123,7 @@ public static class DefaultExclusiveConsumerLogger implements ConditionalExcepti @Override public void log(Log logger, String message, Throwable cause) { if (logger.isDebugEnabled()) { - logger.debug(message + ": " + cause.toString()); + logger.debug(message + ": " + cause); } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java index f6e64c5791..3e9c3ca17a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java @@ -98,7 +98,7 @@ public class BlockingQueueConsumer { private static final int DEFAULT_RETRY_DECLARATION_INTERVAL = 60000; - private static final Log logger = LogFactory.getLog(BlockingQueueConsumer.class); + private static Log logger = LogFactory.getLog(BlockingQueueConsumer.class); private final Lock lifecycleLock = new ReentrantLock(); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java index 3f1e21b229..7cdc31697a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java @@ -36,6 +36,8 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -268,7 +270,7 @@ public void addQueues(Queue... queues) { Assert.noNullElements(queues, "'queues' cannot contain null elements"); try { Arrays.stream(queues) - .map(q -> q.getActualName()) + .map(Queue::getActualName) .forEach(this.removedQueues::remove); addQueues(Arrays.stream(queues).map(Queue::getName)); } @@ -472,7 +474,7 @@ protected void checkConnect() { if (isPossibleAuthenticationFailureFatal()) { Connection connection = null; try { - getConnectionFactory().createConnection(); + connection = getConnectionFactory().createConnection(); } catch (AmqpAuthenticationException ex) { this.logger.debug("Failed to authenticate", ex); @@ -931,7 +933,8 @@ private void cancelConsumer(SimpleConsumer consumer) { if (this.logger.isDebugEnabled()) { this.logger.debug("Canceling " + consumer); } - synchronized (consumer) { + consumer.lock.lock(); + try { consumer.setCanceled(true); if (this.messagesPerAck > 1) { try { @@ -942,6 +945,9 @@ private void cancelConsumer(SimpleConsumer consumer) { } } } + finally { + consumer.lock.unlock(); + } RabbitUtils.cancel(consumer.getChannel(), consumer.getConsumerTag()); } finally { @@ -968,7 +974,7 @@ protected void consumerRemoved(SimpleConsumer consumer) { /** * The consumer object. */ - final class SimpleConsumer extends DefaultConsumer { + protected final class SimpleConsumer extends DefaultConsumer { private final Log logger = DirectMessageListenerContainer.this.logger; @@ -994,6 +1000,8 @@ final class SimpleConsumer extends DefaultConsumer { private final Channel targetChannel; + private final Lock lock = new ReentrantLock(); + private int pendingAcks; private long lastAck = System.currentTimeMillis(); @@ -1230,11 +1238,15 @@ private void handleAck(long deliveryTag, boolean channelLocallyTransacted) { try { if (this.ackRequired) { if (this.messagesPerAck > 1) { - synchronized (this) { + this.lock.lock(); + try { this.latestDeferredDeliveryTag = deliveryTag; this.pendingAcks++; ackIfNecessary(this.lastAck); } + finally { + this.lock.unlock(); + } } else if (!isChannelTransacted() || isLocallyTransacted) { sendAckWithNotify(deliveryTag, false); @@ -1256,7 +1268,7 @@ else if (!isChannelTransacted() || isLocallyTransacted) { * @param now the current time. * @throws IOException if one occurs. */ - synchronized void ackIfNecessary(long now) throws Exception { // NOSONAR + void ackIfNecessary(long now) throws Exception { // NOSONAR if (this.pendingAcks >= this.messagesPerAck || ( this.pendingAcks > 0 && (now - this.lastAck > this.ackTimeout || this.canceled))) { sendAck(now); @@ -1270,11 +1282,15 @@ private void rollback(long deliveryTag, Exception e) { if (this.ackRequired || ContainerUtils.isRejectManual(e)) { try { if (this.messagesPerAck > 1) { - synchronized (this) { + this.lock.lock(); + try { if (this.pendingAcks > 0) { sendAck(System.currentTimeMillis()); } } + finally { + this.lock.unlock(); + } } getChannel().basicNack(deliveryTag, !isAsyncReplies(), ContainerUtils.shouldRequeue(isDefaultRequeueRejected(), e, this.logger)); @@ -1288,7 +1304,7 @@ private void rollback(long deliveryTag, Exception e) { } } - protected synchronized void sendAck(long now) throws Exception { // NOSONAR + void sendAck(long now) throws Exception { // NOSONAR sendAckWithNotify(this.latestDeferredDeliveryTag, true); this.lastAck = now; this.pendingAcks = 0; @@ -1298,8 +1314,8 @@ protected synchronized void sendAck(long now) throws Exception { // NOSONAR * Send ack and notify MessageAckListener(if set). * @param deliveryTag DeliveryTag of this ack. * @param multiple Whether multiple ack. - * @throws Exception Occured when ack. - * @Since 2.4.6 + * @throws Exception Occurred when ack. + * @since 2.4.6 */ private void sendAckWithNotify(long deliveryTag, boolean multiple) throws Exception { // NOSONAR try { @@ -1314,7 +1330,6 @@ private void sendAckWithNotify(long deliveryTag, boolean multiple) throws Except /** * Notify MessageAckListener set on message listener. - * @param messageAckListener MessageAckListener set on the message listener. * @param success Whether ack succeeded. * @param deliveryTag The deliveryTag of ack. * @param cause If an exception occurs. @@ -1325,7 +1340,7 @@ private void notifyMessageAckListener(boolean success, long deliveryTag, @Nullab getMessageAckListener().onComplete(success, deliveryTag, cause); } catch (Exception e) { - this.logger.error("An exception occured on MessageAckListener.", e); + this.logger.error("An exception occurred on MessageAckListener.", e); } } @@ -1412,14 +1427,11 @@ public boolean equals(Object obj) { return false; } if (this.queue == null) { - if (other.queue != null) { - return false; - } + return other.queue == null; } - else if (!this.queue.equals(other.queue)) { - return false; + else { + return this.queue.equals(other.queue); } - return true; } private DirectMessageListenerContainer getEnclosingInstance() { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistrar.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistrar.java index 99e20ff77b..6075b7603a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistrar.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2023 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. @@ -20,6 +20,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; @@ -37,13 +39,16 @@ * @author Stephane Nicoll * @author Juergen Hoeller * @author Artem Bilan + * * @since 1.4 + * * @see org.springframework.amqp.rabbit.annotation.RabbitListenerConfigurer */ public class RabbitListenerEndpointRegistrar implements BeanFactoryAware, InitializingBean { - private final List endpointDescriptors = - new ArrayList(); + private final List endpointDescriptors = new ArrayList<>(); + + private final Lock endpointDescriptorsLock = new ReentrantLock(); private List customMethodArgumentResolvers = new ArrayList<>(); @@ -113,8 +118,7 @@ public void setCustomMethodArgumentResolvers(HandlerMethodArgumentResolver... me * @param rabbitHandlerMethodFactory the {@link MessageHandlerMethodFactory} instance. */ public void setMessageHandlerMethodFactory(MessageHandlerMethodFactory rabbitHandlerMethodFactory) { - Assert.isNull(this.validator, - "A validator cannot be provided with a custom message handler factory"); + Assert.isNull(this.validator, "A validator cannot be provided with a custom message handler factory"); this.messageHandlerMethodFactory = rabbitHandlerMethodFactory; } @@ -186,7 +190,8 @@ public void afterPropertiesSet() { protected void registerAllEndpoints() { Assert.state(this.endpointRegistry != null, "No registry available"); - synchronized (this.endpointDescriptors) { + this.endpointDescriptorsLock.lock(); + try { for (AmqpListenerEndpointDescriptor descriptor : this.endpointDescriptors) { if (descriptor.endpoint instanceof MultiMethodRabbitListenerEndpoint multi && this.validator != null) { multi.setValidator(this.validator); @@ -196,6 +201,9 @@ protected void registerAllEndpoints() { } this.startImmediately = true; // trigger immediate startup } + finally { + this.endpointDescriptorsLock.unlock(); + } } private RabbitListenerContainerFactory resolveContainerFactory(AmqpListenerEndpointDescriptor descriptor) { @@ -233,7 +241,8 @@ public void registerEndpoint(RabbitListenerEndpoint endpoint, Assert.state(!this.startImmediately || this.endpointRegistry != null, "No registry available"); // Factory may be null, we defer the resolution right before actually creating the container AmqpListenerEndpointDescriptor descriptor = new AmqpListenerEndpointDescriptor(endpoint, factory); - synchronized (this.endpointDescriptors) { + this.endpointDescriptorsLock.lock(); + try { if (this.startImmediately) { // Register and start immediately this.endpointRegistry.registerListenerContainer(descriptor.endpoint, // NOSONAR never null resolveContainerFactory(descriptor), true); @@ -242,6 +251,9 @@ public void registerEndpoint(RabbitListenerEndpoint endpoint, this.endpointDescriptors.add(descriptor); } } + finally { + this.endpointDescriptorsLock.unlock(); + } } /** @@ -256,18 +268,15 @@ public void registerEndpoint(RabbitListenerEndpoint endpoint) { } - private static final class AmqpListenerEndpointDescriptor { - - private final RabbitListenerEndpoint endpoint; + private record AmqpListenerEndpointDescriptor(RabbitListenerEndpoint endpoint, + RabbitListenerContainerFactory containerFactory) { - private final RabbitListenerContainerFactory containerFactory; + private AmqpListenerEndpointDescriptor(RabbitListenerEndpoint endpoint, + @Nullable RabbitListenerContainerFactory containerFactory) { + this.endpoint = endpoint; + this.containerFactory = containerFactory; + } - AmqpListenerEndpointDescriptor(RabbitListenerEndpoint endpoint, - @Nullable RabbitListenerContainerFactory containerFactory) { - this.endpoint = endpoint; - this.containerFactory = containerFactory; } - } - } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistry.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistry.java index 7765c3acc9..cf3b2c2615 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistry.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2023 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. @@ -24,6 +24,8 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -57,7 +59,9 @@ * @author Juergen Hoeller * @author Artem Bilan * @author Gary Russell + * * @since 1.4 + * * @see RabbitListenerEndpoint * @see MessageListenerContainer * @see RabbitListenerContainerFactory @@ -67,8 +71,9 @@ public class RabbitListenerEndpointRegistry implements DisposableBean, SmartLife protected final Log logger = LogFactory.getLog(getClass()); // NOSONAR protected - private final Map listenerContainers = - new ConcurrentHashMap(); + private final Map listenerContainers = new ConcurrentHashMap<>(); + + private final Lock listenerContainersLock = new ReentrantLock(); private int phase = Integer.MAX_VALUE; @@ -116,8 +121,7 @@ public Collection getListenerContainers() { /** * Create a message listener container for the given {@link RabbitListenerEndpoint}. - *

This create the necessary infrastructure to honor that endpoint - * with regards to its configuration. + *

This create the necessary infrastructure to honor that endpoint in regard to its configuration. * @param endpoint the endpoint to add * @param factory the listener factory to use * @see #registerListenerContainer(RabbitListenerEndpoint, RabbitListenerContainerFactory, boolean) @@ -128,8 +132,7 @@ public void registerListenerContainer(RabbitListenerEndpoint endpoint, RabbitLis /** * Create a message listener container for the given {@link RabbitListenerEndpoint}. - *

This create the necessary infrastructure to honor that endpoint - * with regards to its configuration. + *

This create the necessary infrastructure to honor that endpoint in regard to its configuration. *

The {@code startImmediately} flag determines if the container should be * started immediately. * @param endpoint the endpoint to add. @@ -141,12 +144,14 @@ public void registerListenerContainer(RabbitListenerEndpoint endpoint, RabbitLis @SuppressWarnings("unchecked") public void registerListenerContainer(RabbitListenerEndpoint endpoint, RabbitListenerContainerFactory factory, boolean startImmediately) { + Assert.notNull(endpoint, "Endpoint must not be null"); Assert.notNull(factory, "Factory must not be null"); String id = endpoint.getId(); Assert.hasText(id, "Endpoint id must not be empty"); - synchronized (this.listenerContainers) { + this.listenerContainersLock.lock(); + try { Assert.state(!this.listenerContainers.containsKey(id), "Another endpoint is already registered with id '" + id + "'"); MessageListenerContainer container = createListenerContainer(endpoint, factory); @@ -157,7 +162,7 @@ public void registerListenerContainer(RabbitListenerEndpoint endpoint, RabbitLis containerGroup = this.applicationContext.getBean(endpoint.getGroup(), List.class); } else { - containerGroup = new ArrayList(); + containerGroup = new ArrayList<>(); this.applicationContext.getBeanFactory().registerSingleton(endpoint.getGroup(), containerGroup); } containerGroup.add(container); @@ -169,6 +174,9 @@ public void registerListenerContainer(RabbitListenerEndpoint endpoint, RabbitLis startIfNecessary(container); } } + finally { + this.listenerContainersLock.unlock(); + } } /** @@ -250,7 +258,7 @@ public void stop() { @Override public void stop(Runnable callback) { Collection containers = getListenerContainers(); - if (containers.size() > 0) { + if (!containers.isEmpty()) { AggregatingCallback aggregatingCallback = new AggregatingCallback(containers.size(), callback); for (MessageListenerContainer listenerContainer : containers) { try { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java index 91325fde86..9007bfda0d 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java @@ -1152,12 +1152,16 @@ private void executeWithList(Channel channel, List messages, long deliv protected void handleStartupFailure(BackOffExecution backOffExecution) { long recoveryInterval = backOffExecution.nextBackOff(); if (BackOffExecution.STOP == recoveryInterval) { - synchronized (this) { + this.lifecycleLock.lock(); + try { if (isActive()) { logger.warn("stopping container - restart recovery attempts exhausted"); stop(); } } + finally { + this.lifecycleLock.unlock(); + } return; } try { From b6dbba3ee4eb1e1f6a19c8232424e66de8c4c79c Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 18 Dec 2023 11:27:57 -0500 Subject: [PATCH 331/737] Add dependabot support --- .github/dependabot.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..d61b9ca591 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,28 @@ +version: 2 +updates: + - package-ecosystem: "gradle" + directory: "/" + schedule: + interval: "weekly" + day: "saturday" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major", "version-update:semver-minor"] + open-pull-requests-limit: 10 + groups: + development-dependencies: + update-types: "patch" + patterns: + - "com.gradle.enterprise" + - "io.spring.*" + - "org.ajoberstar.grgit" + - "org.antora" + - "io.micrometer:micrometer-docs-generator" + - "com.willowtreeapps.assertk:assertk-jvm" + - "org.hibernate.validator:hibernate-validator" + - "org.apache.httpcomponents.client5:httpclient5" + - "org.awaitility:awaitility" + - "com.google.code.findbugs:jsr305" + - "org.xerial.snappy:snappy-java" + - "org.lz4:lz4-java" + - "com.github.luben:zstd-jni" From 02867e66560b8eb0b4fdc604142a0a2df5a5b36d Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 18 Dec 2023 11:31:39 -0500 Subject: [PATCH 332/737] Fix `dependabot.yml` syntax --- .github/dependabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d61b9ca591..597dfc403c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,7 +11,7 @@ updates: open-pull-requests-limit: 10 groups: development-dependencies: - update-types: "patch" + update-types: ["patch"] patterns: - "com.gradle.enterprise" - "io.spring.*" From ef9f37d670260423495e650d14f84a5c068cc03b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 11:40:50 -0500 Subject: [PATCH 333/737] Bump the development-dependencies group Bumps the development-dependencies group with 3 updates: [org.apache.httpcomponents.client5:httpclient5](https://github.com/apache/httpcomponents-client), [org.xerial.snappy:snappy-java](https://github.com/xerial/snappy-java) and [com.github.luben:zstd-jni](https://github.com/luben/zstd-jni). Updates `org.apache.httpcomponents.client5:httpclient5` from 5.2.1 to 5.2.3 - [Changelog](https://github.com/apache/httpcomponents-client/blob/rel/v5.2.3/RELEASE_NOTES.txt) - [Commits](https://github.com/apache/httpcomponents-client/compare/rel/v5.2.1...rel/v5.2.3) Updates `org.xerial.snappy:snappy-java` from 1.1.8.4 to 1.1.10.5 - [Release notes](https://github.com/xerial/snappy-java/releases) - [Commits](https://github.com/xerial/snappy-java/compare/1.1.8.4...v1.1.10.5) Updates `com.github.luben:zstd-jni` from 1.5.0-2 to 1.5.5-11 - [Commits](https://github.com/luben/zstd-jni/compare/v1.5.0-2...v1.5.5-11) --- updated-dependencies: - dependency-name: org.apache.httpcomponents.client5:httpclient5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies - dependency-name: org.xerial.snappy:snappy-java dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies - dependency-name: com.github.luben:zstd-jni dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index b9c704d91a..4c992d9994 100644 --- a/build.gradle +++ b/build.gradle @@ -47,7 +47,7 @@ ext { assertkVersion = '0.27.0' awaitilityVersion = '4.2.0' commonsCompressVersion = '1.20' - commonsHttpClientVersion = '5.2.1' + commonsHttpClientVersion = '5.2.3' commonsPoolVersion = '2.12.0' googleJsr305Version = '3.0.2' hamcrestVersion = '2.2' @@ -67,12 +67,12 @@ ext { rabbitmqStreamVersion = '0.14.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.19.0' reactorVersion = '2023.0.0' - snappyVersion = '1.1.8.4' + snappyVersion = '1.1.10.5' springDataVersion = '2023.1.0' springRetryVersion = '2.0.4' springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.1.0' testcontainersVersion = '1.19.1' - zstdJniVersion = '1.5.0-2' + zstdJniVersion = '1.5.5-11' javaProjects = subprojects - project(':spring-amqp-bom') } From d6b9a56f92fb91a689c43ea06ae2ece3be3a5d3f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 11:41:54 -0500 Subject: [PATCH 334/737] Bump org.springframework.data:spring-data-bom from 2023.1.0 to 2023.1.1 (#2580) Bumps [org.springframework.data:spring-data-bom](https://github.com/spring-projects/spring-data-bom) from 2023.1.0 to 2023.1.1. - [Release notes](https://github.com/spring-projects/spring-data-bom/releases) - [Commits](https://github.com/spring-projects/spring-data-bom/compare/2023.1.0...2023.1.1) --- updated-dependencies: - dependency-name: org.springframework.data:spring-data-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4c992d9994..dcd3173693 100644 --- a/build.gradle +++ b/build.gradle @@ -68,7 +68,7 @@ ext { rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.19.0' reactorVersion = '2023.0.0' snappyVersion = '1.1.10.5' - springDataVersion = '2023.1.0' + springDataVersion = '2023.1.1' springRetryVersion = '2.0.4' springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.1.0' testcontainersVersion = '1.19.1' From e3da11b397166f078f52f0f7f4efe8c4efbf7e6e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 11:42:14 -0500 Subject: [PATCH 335/737] Bump org.junit:junit-bom from 5.10.0 to 5.10.1 (#2581) Bumps [org.junit:junit-bom](https://github.com/junit-team/junit5) from 5.10.0 to 5.10.1. - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.10.0...r5.10.1) --- updated-dependencies: - dependency-name: org.junit:junit-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index dcd3173693..7ad30666df 100644 --- a/build.gradle +++ b/build.gradle @@ -55,7 +55,7 @@ ext { jacksonBomVersion = '2.15.3' jaywayJsonPathVersion = '2.8.0' junit4Version = '4.13.2' - junitJupiterVersion = '5.10.0' + junitJupiterVersion = '5.10.1' kotlinCoroutinesVersion = '1.7.3' log4jVersion = '2.21.1' logbackVersion = '1.4.11' From 623f4db9f7ae7253126a417ae3f1a4ddcca7a3d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 11:42:39 -0500 Subject: [PATCH 336/737] Bump io.micrometer:micrometer-bom from 1.12.0 to 1.12.1 (#2588) Bumps [io.micrometer:micrometer-bom](https://github.com/micrometer-metrics/micrometer) from 1.12.0 to 1.12.1. - [Release notes](https://github.com/micrometer-metrics/micrometer/releases) - [Commits](https://github.com/micrometer-metrics/micrometer/compare/v1.12.0...v1.12.1) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 7ad30666df..afeb05bb15 100644 --- a/build.gradle +++ b/build.gradle @@ -61,7 +61,7 @@ ext { logbackVersion = '1.4.11' lz4Version = '1.8.0' micrometerDocsVersion = '1.0.2' - micrometerVersion = '1.12.0' + micrometerVersion = '1.12.1' micrometerTracingVersion = '1.2.0' mockitoVersion = '5.6.0' rabbitmqStreamVersion = '0.14.0' From 706108188decddaae04e3d0ce521fb272130c72b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 11:43:08 -0500 Subject: [PATCH 337/737] Bump ch.qos.logback:logback-classic from 1.4.11 to 1.4.14 (#2585) Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.4.11 to 1.4.14. - [Commits](https://github.com/qos-ch/logback/compare/v_1.4.11...v_1.4.14) --- updated-dependencies: - dependency-name: ch.qos.logback:logback-classic dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index afeb05bb15..6834c4957d 100644 --- a/build.gradle +++ b/build.gradle @@ -58,7 +58,7 @@ ext { junitJupiterVersion = '5.10.1' kotlinCoroutinesVersion = '1.7.3' log4jVersion = '2.21.1' - logbackVersion = '1.4.11' + logbackVersion = '1.4.14' lz4Version = '1.8.0' micrometerDocsVersion = '1.0.2' micrometerVersion = '1.12.1' From f8579088a2bd0fd00f6d52134f8f90ca8a72fb8c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 11:43:26 -0500 Subject: [PATCH 338/737] Bump org.springframework.retry:spring-retry from 2.0.4 to 2.0.5 (#2582) Bumps [org.springframework.retry:spring-retry](https://github.com/spring-projects/spring-retry) from 2.0.4 to 2.0.5. - [Release notes](https://github.com/spring-projects/spring-retry/releases) - [Commits](https://github.com/spring-projects/spring-retry/compare/v2.0.4...v2.0.5) --- updated-dependencies: - dependency-name: org.springframework.retry:spring-retry dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 6834c4957d..845bbf6810 100644 --- a/build.gradle +++ b/build.gradle @@ -69,7 +69,7 @@ ext { reactorVersion = '2023.0.0' snappyVersion = '1.1.10.5' springDataVersion = '2023.1.1' - springRetryVersion = '2.0.4' + springRetryVersion = '2.0.5' springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.1.0' testcontainersVersion = '1.19.1' zstdJniVersion = '1.5.5-11' From 396d062fa7ebf57886a99bf8e21d305764e493dd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 11:43:50 -0500 Subject: [PATCH 339/737] Bump kotlinVersion from 1.9.20 to 1.9.21 (#2583) Bumps `kotlinVersion` from 1.9.20 to 1.9.21. Updates `org.jetbrains.kotlin:kotlin-gradle-plugin` from 1.9.20 to 1.9.21 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.9.20...v1.9.21) Updates `org.jetbrains.kotlin:kotlin-allopen` from 1.9.20 to 1.9.21 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.9.20...v1.9.21) --- updated-dependencies: - dependency-name: org.jetbrains.kotlin:kotlin-gradle-plugin dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.jetbrains.kotlin:kotlin-allopen dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 845bbf6810..5fa6993c4f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlinVersion = '1.9.20' + ext.kotlinVersion = '1.9.21' ext.isCI = System.getenv('GITHUB_ACTION') repositories { mavenCentral() From 9b8067bde071e7f58c9e08f6bb34e8326e70d9cd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 11:44:25 -0500 Subject: [PATCH 340/737] Bump org.testcontainers:testcontainers-bom from 1.19.1 to 1.19.3 (#2584) Bumps [org.testcontainers:testcontainers-bom](https://github.com/testcontainers/testcontainers-java) from 1.19.1 to 1.19.3. - [Release notes](https://github.com/testcontainers/testcontainers-java/releases) - [Changelog](https://github.com/testcontainers/testcontainers-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/testcontainers/testcontainers-java/compare/1.19.1...1.19.3) --- updated-dependencies: - dependency-name: org.testcontainers:testcontainers-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 5fa6993c4f..df1e49e189 100644 --- a/build.gradle +++ b/build.gradle @@ -71,7 +71,7 @@ ext { springDataVersion = '2023.1.1' springRetryVersion = '2.0.5' springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.1.0' - testcontainersVersion = '1.19.1' + testcontainersVersion = '1.19.3' zstdJniVersion = '1.5.5-11' javaProjects = subprojects - project(':spring-amqp-bom') From a1e40fd4960f955dbc567c0a1822d31619f76fd1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 11:44:45 -0500 Subject: [PATCH 341/737] Bump io.projectreactor:reactor-bom from 2023.0.0 to 2023.0.1 (#2586) Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2023.0.0 to 2023.0.1. - [Release notes](https://github.com/reactor/reactor/releases) - [Commits](https://github.com/reactor/reactor/compare/2023.0.0...2023.0.1) --- updated-dependencies: - dependency-name: io.projectreactor:reactor-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index df1e49e189..73492e5bae 100644 --- a/build.gradle +++ b/build.gradle @@ -66,7 +66,7 @@ ext { mockitoVersion = '5.6.0' rabbitmqStreamVersion = '0.14.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.19.0' - reactorVersion = '2023.0.0' + reactorVersion = '2023.0.1' snappyVersion = '1.1.10.5' springDataVersion = '2023.1.1' springRetryVersion = '2.0.5' From 1545cf87791e10e8d05278d8fc0d00db03cfb2a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 11:45:05 -0500 Subject: [PATCH 342/737] Bump io.micrometer:micrometer-tracing-bom from 1.2.0 to 1.2.1 (#2587) Bumps [io.micrometer:micrometer-tracing-bom](https://github.com/micrometer-metrics/tracing) from 1.2.0 to 1.2.1. - [Release notes](https://github.com/micrometer-metrics/tracing/releases) - [Commits](https://github.com/micrometer-metrics/tracing/compare/v1.2.0...v1.2.1) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-tracing-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 73492e5bae..f1e2b15f5e 100644 --- a/build.gradle +++ b/build.gradle @@ -62,7 +62,7 @@ ext { lz4Version = '1.8.0' micrometerDocsVersion = '1.0.2' micrometerVersion = '1.12.1' - micrometerTracingVersion = '1.2.0' + micrometerTracingVersion = '1.2.1' mockitoVersion = '5.6.0' rabbitmqStreamVersion = '0.14.0' rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.19.0' From e22e1c8b438030cc165c8977be47255fc9ba6f5a Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 18 Dec 2023 11:48:31 -0500 Subject: [PATCH 343/737] Remove project props for deps --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index f1e2b15f5e..78b7725634 100644 --- a/build.gradle +++ b/build.gradle @@ -65,12 +65,12 @@ ext { micrometerTracingVersion = '1.2.1' mockitoVersion = '5.6.0' rabbitmqStreamVersion = '0.14.0' - rabbitmqVersion = project.hasProperty('rabbitmqVersion') ? project.rabbitmqVersion : '5.19.0' + rabbitmqVersion = '5.19.0' reactorVersion = '2023.0.1' snappyVersion = '1.1.10.5' springDataVersion = '2023.1.1' springRetryVersion = '2.0.5' - springVersion = project.hasProperty('springVersion') ? project.springVersion : '6.1.0' + springVersion = '6.1.0' testcontainersVersion = '1.19.3' zstdJniVersion = '1.5.5-11' From 6215b96242019a6513b72fce6cab3a3dec52f38d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 11:50:58 -0500 Subject: [PATCH 344/737] Bump org.springframework:spring-framework-bom from 6.1.0 to 6.1.2 (#2589) Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.1.0 to 6.1.2. - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.1.0...v6.1.2) --- updated-dependencies: - dependency-name: org.springframework:spring-framework-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 78b7725634..a23d04a3cb 100644 --- a/build.gradle +++ b/build.gradle @@ -70,7 +70,7 @@ ext { snappyVersion = '1.1.10.5' springDataVersion = '2023.1.1' springRetryVersion = '2.0.5' - springVersion = '6.1.0' + springVersion = '6.1.2' testcontainersVersion = '1.19.3' zstdJniVersion = '1.5.5-11' From 33bd06cbf28fdb3b8a4ce3968b33958b03f512fa Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 18 Dec 2023 18:10:54 +0000 Subject: [PATCH 345/737] [artifactory-release] Release version 3.1.1 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index bf6d0d0664..ae3f47f734 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.1.1-SNAPSHOT +version=3.1.1 org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true From c981b342e5299cf9b54bc2cbec0981af5e92af7b Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 18 Dec 2023 18:10:56 +0000 Subject: [PATCH 346/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index ae3f47f734..01eb10e1e3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.1.1 +version=3.1.2-SNAPSHOT org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true From 0ba5d46abead47a9a1bffed2559d6a6e9abcd3aa Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 18 Dec 2023 13:28:52 -0500 Subject: [PATCH 347/737] Add RabbitMQ service to the verify-staged WF --- .github/workflows/verify-staged-artifacts.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/verify-staged-artifacts.yml b/.github/workflows/verify-staged-artifacts.yml index b0eb786756..58ee4f83e8 100644 --- a/.github/workflows/verify-staged-artifacts.yml +++ b/.github/workflows/verify-staged-artifacts.yml @@ -11,6 +11,15 @@ on: jobs: verify-staged-with-samples: runs-on: ubuntu-latest + + services: + + rabbitmq: + image: rabbitmq:management + ports: + - 5672:5672 + - 15672:15672 + steps: - name: Checkout Samples Repo From 7ed833146e038cd53499eaada1aebe784c2bd912 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 18 Dec 2023 14:41:53 -0500 Subject: [PATCH 348/737] Add tentative `manual-finalize-release.yml` --- .github/workflows/manual-finalize-release.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/manual-finalize-release.yml diff --git a/.github/workflows/manual-finalize-release.yml b/.github/workflows/manual-finalize-release.yml new file mode 100644 index 0000000000..53456d66bb --- /dev/null +++ b/.github/workflows/manual-finalize-release.yml @@ -0,0 +1,19 @@ +name: Finalize Release Manually + +on: + workflow_dispatch: + inputs: + milestone: + description: 'Milestone title, e.g 3.0.0-M1, 3.1.0-RC1, 3.2.0 etc.' + +jobs: + finalize-release: + permissions: + actions: write + + uses: artembilan/spring-github-workflows/.github/workflows/spring-finalize-release.yml@main + with: + milestone: ${{ inputs.milestone }} + secrets: + GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} + SPRING_RELEASE_SLACK_WEBHOOK_URL: ${{ secrets.SPRING_RELEASE_SLACK_WEBHOOK_URL }} \ No newline at end of file From 5d6a0d82f048908938da0738de6ebd5f179f26cd Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 18 Dec 2023 15:42:31 -0500 Subject: [PATCH 349/737] Add `contents: write` to create GH releases --- .github/workflows/manual-finalize-release.yml | 1 + .github/workflows/release.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/manual-finalize-release.yml b/.github/workflows/manual-finalize-release.yml index 53456d66bb..719f1930a6 100644 --- a/.github/workflows/manual-finalize-release.yml +++ b/.github/workflows/manual-finalize-release.yml @@ -10,6 +10,7 @@ jobs: finalize-release: permissions: actions: write + contents: write uses: artembilan/spring-github-workflows/.github/workflows/spring-finalize-release.yml@main with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f2c3c9b3d0..b7613fdedb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,6 +9,7 @@ jobs: release: permissions: actions: write + contents: write uses: artembilan/spring-github-workflows/.github/workflows/spring-artifactory-gradle-release.yml@main secrets: From 58ba13bc12c86122b4d07b124a947bfd9b116d23 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 18 Dec 2023 17:45:17 -0500 Subject: [PATCH 350/737] Add `issues: write` to create GH releases * Fix `dependabot.yml` to use expected by the changelog-generator label --- .github/dependabot.yml | 1 + .github/workflows/manual-finalize-release.yml | 20 ------------------- .github/workflows/release.yml | 1 + 3 files changed, 2 insertions(+), 20 deletions(-) delete mode 100644 .github/workflows/manual-finalize-release.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 597dfc403c..91d3d1e368 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,6 +9,7 @@ updates: - dependency-name: "*" update-types: ["version-update:semver-major", "version-update:semver-minor"] open-pull-requests-limit: 10 + labels: ["type: dependency-upgrade"] groups: development-dependencies: update-types: ["patch"] diff --git a/.github/workflows/manual-finalize-release.yml b/.github/workflows/manual-finalize-release.yml deleted file mode 100644 index 719f1930a6..0000000000 --- a/.github/workflows/manual-finalize-release.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Finalize Release Manually - -on: - workflow_dispatch: - inputs: - milestone: - description: 'Milestone title, e.g 3.0.0-M1, 3.1.0-RC1, 3.2.0 etc.' - -jobs: - finalize-release: - permissions: - actions: write - contents: write - - uses: artembilan/spring-github-workflows/.github/workflows/spring-finalize-release.yml@main - with: - milestone: ${{ inputs.milestone }} - secrets: - GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} - SPRING_RELEASE_SLACK_WEBHOOK_URL: ${{ secrets.SPRING_RELEASE_SLACK_WEBHOOK_URL }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b7613fdedb..0988fc9f9a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,6 +10,7 @@ jobs: permissions: actions: write contents: write + issues: write uses: artembilan/spring-github-workflows/.github/workflows/spring-artifactory-gradle-release.yml@main secrets: From 8a5f83afc30b5a76681cd51897d4aa38f53df903 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 20 Dec 2023 17:01:26 -0500 Subject: [PATCH 351/737] Migrate GHAs for repo from Spring IO --- .github/workflows/backport-issue.yml | 2 +- .github/workflows/ci-snapshot.yml | 2 +- .github/workflows/pr-build.yml | 2 +- .github/workflows/release.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/backport-issue.yml b/.github/workflows/backport-issue.yml index 59326f48d8..c32d9bf190 100644 --- a/.github/workflows/backport-issue.yml +++ b/.github/workflows/backport-issue.yml @@ -7,6 +7,6 @@ on: jobs: backport-issue: - uses: artembilan/spring-github-workflows/.github/workflows/spring-backport-issue.yml@main + uses: spring-io/spring-github-workflows/.github/workflows/spring-backport-issue.yml@v1 secrets: GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/ci-snapshot.yml b/.github/workflows/ci-snapshot.yml index 74ed09a830..f553544872 100644 --- a/.github/workflows/ci-snapshot.yml +++ b/.github/workflows/ci-snapshot.yml @@ -9,7 +9,7 @@ on: jobs: build-snapshot: - uses: artembilan/spring-github-workflows/.github/workflows/spring-artifactory-gradle-snapshot.yml@main + uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-snapshot.yml@v1 secrets: GRADLE_ENTERPRISE_CACHE_USER: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 246f6a7f11..945b2f94d9 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -7,4 +7,4 @@ on: jobs: build-pull-request: - uses: artembilan/spring-github-workflows/.github/workflows/spring-gradle-pull-request-build.yml@main + uses: spring-io/spring-github-workflows/.github/workflows/spring-gradle-pull-request-build.yml@v1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0988fc9f9a..aa2cfbcdd9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: contents: write issues: write - uses: artembilan/spring-github-workflows/.github/workflows/spring-artifactory-gradle-release.yml@main + uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-release.yml@v1 secrets: GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} GRADLE_ENTERPRISE_CACHE_USER: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} From c4cf3662817617594e7b92dc805ac86ae38065fa Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 22 Dec 2023 13:34:23 -0500 Subject: [PATCH 352/737] Migrate deploy-docs to reusable WF from Spring IO --- .github/workflows/deploy-docs.yml | 35 ++++++++++--------------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index f3e899c4fe..2b7e795b26 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -1,32 +1,19 @@ name: Deploy Docs + on: push: - branches-ignore: [ gh-pages ] - tags: '**' - repository_dispatch: - types: request-build-reference # legacy - schedule: - - cron: '0 10 * * *' # Once per day at 10am UTC + branches: + - '*.x' + - main + tags: + - '**' + workflow_dispatch: + permissions: actions: write + jobs: - build: - runs-on: ubuntu-latest + dispatch-docs-build: if: github.repository_owner == 'spring-projects' - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - ref: docs-build - fetch-depth: 1 - - name: Dispatch (partial build) - if: github.ref_type == 'branch' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD) -f build-refname=${{ github.ref_name }} - - name: Dispatch (full build) - if: github.ref_type == 'tag' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD) + uses: spring-io/spring-github-workflows/.github/workflows/spring-deploy-docs.yml@main From 22b2cc3895f067a846d07df6584ff170ae47ef01 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 22 Dec 2023 13:35:10 -0500 Subject: [PATCH 353/737] Add `merge-dependabot-pr.yml` --- .github/workflows/merge-dependabot-pr.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/workflows/merge-dependabot-pr.yml diff --git a/.github/workflows/merge-dependabot-pr.yml b/.github/workflows/merge-dependabot-pr.yml new file mode 100644 index 0000000000..7dfa29e2e0 --- /dev/null +++ b/.github/workflows/merge-dependabot-pr.yml @@ -0,0 +1,14 @@ +name: Merge Dependabot PR + +on: + pull_request: + branches: + - main + +run-name: Merge Dependabot PR ${{ github.ref_name }} + +jobs: + merge-dependabot-pr: + permissions: write-all + + uses: spring-io/spring-github-workflows/.github/workflows/spring-merge-dependabot-pr.yml@v1 \ No newline at end of file From dbde56f7fff7fd0edb32202a4a403ac7c6cc6796 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 22 Dec 2023 14:57:20 -0500 Subject: [PATCH 354/737] Use `spring-dispatch-docs-build.yml` --- .github/workflows/deploy-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 2b7e795b26..1771c58265 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -16,4 +16,4 @@ permissions: jobs: dispatch-docs-build: if: github.repository_owner == 'spring-projects' - uses: spring-io/spring-github-workflows/.github/workflows/spring-deploy-docs.yml@main + uses: spring-io/spring-github-workflows/.github/workflows/spring-dispatch-docs-build.yml@main From 0e574aad1c669299b7e38f039f406411012ca9b2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 Dec 2023 22:07:31 -0500 Subject: [PATCH 355/737] Bump kotlinVersion from 1.9.21 to 1.9.22 (#2592) Bumps `kotlinVersion` from 1.9.21 to 1.9.22. Updates `org.jetbrains.kotlin:kotlin-gradle-plugin` from 1.9.21 to 1.9.22 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.9.21...v1.9.22) Updates `org.jetbrains.kotlin:kotlin-allopen` from 1.9.21 to 1.9.22 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.9.21...v1.9.22) --- updated-dependencies: - dependency-name: org.jetbrains.kotlin:kotlin-gradle-plugin dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.jetbrains.kotlin:kotlin-allopen dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a23d04a3cb..0ae032c6da 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlinVersion = '1.9.21' + ext.kotlinVersion = '1.9.22' ext.isCI = System.getenv('GITHUB_ACTION') repositories { mavenCentral() From 6cc91b5b73382706d4e606a69df62b503e09ab25 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 2 Jan 2024 14:33:59 -0500 Subject: [PATCH 356/737] Add concurrency & schedule for ci-snapshot.yml * No need in queued snapshot builds: the latest is enough * Build fresh snapshot every night --- .github/workflows/ci-snapshot.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/ci-snapshot.yml b/.github/workflows/ci-snapshot.yml index f553544872..ca04973710 100644 --- a/.github/workflows/ci-snapshot.yml +++ b/.github/workflows/ci-snapshot.yml @@ -2,14 +2,24 @@ name: CI SNAPSHOT on: workflow_dispatch: + push: branches: - main - '*.x' + schedule: + - cron: '0 5 * * *' + +concurrency: + group: group-snapshot-for-${{ github.ref }} + cancel-in-progress: true + jobs: build-snapshot: uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-snapshot.yml@v1 + with: + gradleTasks: ${{ github.event_name == 'schedule' && '--rerun-tasks' || '' }} secrets: GRADLE_ENTERPRISE_CACHE_USER: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} From 70ba65f2c2b8c32ea11a7257d64e05f9a8389ca2 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 9 Jan 2024 11:21:11 -0500 Subject: [PATCH 357/737] GH-2593: Reliably shutdown SMLC Fixes: #2593 * Add `activeObjectCounter` release into the `BlockingQueueConsumer.handleCancelOk()` in reply to the `basicCancel()` call * Adjust `BlockingQueueConsumer.basicCancel()` to call `RabbitUtils.closeMessageConsumer()` to setisfy transactional context * Adjust `SimpleMessageListenerContainerIntegrationTests` to eventually setisfy to the transaction rollback when container is shuted down * Add new tests into the `ContainerShutDownTests` to verify the listener containers are not blocked waiting on the `cancelationLock` **Cherry-pick to `3.0.x`** --- .../AbstractMessageListenerContainer.java | 4 +- .../listener/BlockingQueueConsumer.java | 12 ++--- .../listener/ContainerShutDownTests.java | 47 +++++++++++++++++-- ...sageListenerContainerIntegrationTests.java | 5 +- .../SimpleMessageListenerContainerTests.java | 11 +++-- 5 files changed, 60 insertions(+), 19 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index a2b698e848..7c3b44618f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -1177,7 +1177,7 @@ protected boolean isForceStop() { /** * Set to true to stop the container after the current message(s) are processed and * requeue any prefetched. Useful when using exclusive or single-active consumers. - * @param forceStop true to stop when current messsage(s) are processed. + * @param forceStop true to stop when current message(s) are processed. * @since 2.4.14 */ public void setForceStop(boolean forceStop) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java index 3e9c3ca17a..de2d5da756 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -465,11 +465,10 @@ protected void basicCancel() { protected void basicCancel(boolean expected) { this.normalCancel = expected; - getConsumerTags().forEach(consumerTag -> { - if (this.channel.isOpen()) { - RabbitUtils.cancel(this.channel, consumerTag); - } - }); + Collection consumerTags = getConsumerTags(); + if (!CollectionUtils.isEmpty(consumerTags)) { + RabbitUtils.closeMessageConsumer(this.channel, consumerTags, this.transactional); + } this.cancelled.set(true); this.abortStarted = System.currentTimeMillis(); } @@ -1007,6 +1006,7 @@ public void handleCancelOk(String consumerTag) { + "); " + BlockingQueueConsumer.this); } this.canceled = true; + BlockingQueueConsumer.this.activeObjectCounter.release(BlockingQueueConsumer.this); } @Override diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerShutDownTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerShutDownTests.java index 6156cb534d..db23cde289 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerShutDownTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerShutDownTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 the original author or authors. + * Copyright 2017-2024 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. @@ -30,11 +30,13 @@ import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.junit.RabbitAvailable; import org.springframework.amqp.utils.test.TestUtils; +import org.springframework.util.StopWatch; import com.rabbitmq.client.AMQP.BasicProperties; /** * @author Gary Russell + * @author Artem Bilan * @since 2.0 * */ @@ -56,7 +58,6 @@ public void testUninterruptibleListenerDMLC() throws Exception { public void testUninterruptibleListener(AbstractMessageListenerContainer container) throws Exception { CachingConnectionFactory cf = new CachingConnectionFactory("localhost"); container.setConnectionFactory(cf); - container.setShutdownTimeout(500); container.setQueueNames("test.shutdown"); final CountDownLatch latch = new CountDownLatch(1); final CountDownLatch testEnded = new CountDownLatch(1); @@ -91,11 +92,49 @@ public void testUninterruptibleListener(AbstractMessageListenerContainer contain assertThat(channels).hasSize(2); } finally { + testEnded.countDown(); container.stop(); - assertThat(channels).hasSize(1); + cf.destroy(); + } + } + + @Test + public void consumersCorrectlyCancelledOnShutdownSMLC() throws Exception { + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); + consumersCorrectlyCancelledOnShutdown(container); + } + @Test + public void consumersCorrectlyCancelledOnShutdownDMLC() throws Exception { + DirectMessageListenerContainer container = new DirectMessageListenerContainer(); + consumersCorrectlyCancelledOnShutdown(container); + } + + private void consumersCorrectlyCancelledOnShutdown(AbstractMessageListenerContainer container) + throws InterruptedException { + + CachingConnectionFactory cf = new CachingConnectionFactory("localhost"); + container.setConnectionFactory(cf); + container.setQueueNames("test.shutdown"); + container.setMessageListener(m -> { + }); + final CountDownLatch startLatch = new CountDownLatch(1); + container.setApplicationEventPublisher(e -> { + if (e instanceof AsyncConsumerStartedEvent) { + startLatch.countDown(); + } + }); + container.start(); + try { + assertThat(startLatch.await(30, TimeUnit.SECONDS)).isTrue(); + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + container.shutdown(); + stopWatch.stop(); + assertThat(stopWatch.getTotalTimeMillis()).isLessThan(3000); + } + finally { cf.destroy(); - testEnded.countDown(); } } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegrationTests.java index ad19a5526d..64fee8a554 100755 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -18,6 +18,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.awaitility.Awaitility.await; import java.util.ArrayList; import java.util.Arrays; @@ -314,7 +315,7 @@ private void doListenerWithExceptionTest(CountDownLatch latch, MessageListener l container.shutdown(); } if (acknowledgeMode.isTransactionAllowed()) { - assertThat(template.receiveAndConvert(queue.getName())).isNotNull(); + await().untilAsserted(() -> assertThat(template.receiveAndConvert(queue.getName())).isNotNull()); } else { assertThat(template.receiveAndConvert(queue.getName())).isNull(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java index 204df12abb..7044f5ee6c 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -50,6 +50,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; @@ -517,7 +518,7 @@ public void testWithConnectionPerListenerThread() throws Exception { waitForConsumersToStop(consumers); Set allocatedConnections = TestUtils.getPropertyValue(ccf, "allocatedConnections", Set.class); assertThat(allocatedConnections).hasSize(2); - assertThat(ccf.getCacheProperties().get("openConnections")).isEqualTo("1"); + assertThat(ccf.getCacheProperties().get("openConnections")).isEqualTo("2"); } @Test @@ -807,15 +808,15 @@ private Answer messageToConsumer(final Channel mockChannel, final Simple } - private void waitForConsumersToStop(Set consumers) throws Exception { + private void waitForConsumersToStop(Set consumers) { with().pollInterval(Duration.ofMillis(10)).atMost(Duration.ofSeconds(10)) .until(() -> consumers.stream() .map(consumer -> TestUtils.getPropertyValue(consumer, "consumer")) - .allMatch(c -> c == null)); + .allMatch(Objects::isNull)); } @SuppressWarnings("serial") - private class TestTransactionManager extends AbstractPlatformTransactionManager { + private static class TestTransactionManager extends AbstractPlatformTransactionManager { @Override protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException { From 039bdd567e98a85d40c37dbdf4b466a414f3cdfc Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 11 Jan 2024 13:31:16 -0500 Subject: [PATCH 358/737] Fix updateCopyright Gradle task to read files in UTF-8 This would avoid non-ASCII symbols breakage on those operating systems where UTF-8 is not default --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0ae032c6da..5a8819b372 100644 --- a/build.gradle +++ b/build.gradle @@ -272,7 +272,7 @@ configure(javaProjects) { subproject -> def beginningYear = matcher[0][1] if (now != beginningYear && now != matcher[0][2]) { def years = "$beginningYear-$now" - def sourceCode = file.text + def sourceCode = file.getText('UTF-8') sourceCode = sourceCode.replaceFirst(/20\d\d(-20\d\d)?/, years) file.text = sourceCode println "Copyright updated for file: $file" From 57b8eb90b079b0483a35291fb60140c3d20d6f80 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Jan 2024 02:21:49 +0000 Subject: [PATCH 359/737] Bump io.projectreactor:reactor-bom from 2023.0.1 to 2023.0.2 (#2597) Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2023.0.1 to 2023.0.2. - [Release notes](https://github.com/reactor/reactor/releases) - [Commits](https://github.com/reactor/reactor/compare/2023.0.1...2023.0.2) --- updated-dependencies: - dependency-name: io.projectreactor:reactor-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 5a8819b372..2a7ffa0875 100644 --- a/build.gradle +++ b/build.gradle @@ -66,7 +66,7 @@ ext { mockitoVersion = '5.6.0' rabbitmqStreamVersion = '0.14.0' rabbitmqVersion = '5.19.0' - reactorVersion = '2023.0.1' + reactorVersion = '2023.0.2' snappyVersion = '1.1.10.5' springDataVersion = '2023.1.1' springRetryVersion = '2.0.5' From 7979ec30b45702dddbd8af11c4cbcf29554c860f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Jan 2024 02:22:10 +0000 Subject: [PATCH 360/737] Bump org.springframework.data:spring-data-bom from 2023.1.1 to 2023.1.2 (#2596) Bumps [org.springframework.data:spring-data-bom](https://github.com/spring-projects/spring-data-bom) from 2023.1.1 to 2023.1.2. - [Release notes](https://github.com/spring-projects/spring-data-bom/releases) - [Commits](https://github.com/spring-projects/spring-data-bom/compare/2023.1.1...2023.1.2) --- updated-dependencies: - dependency-name: org.springframework.data:spring-data-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2a7ffa0875..780f1aeda2 100644 --- a/build.gradle +++ b/build.gradle @@ -68,7 +68,7 @@ ext { rabbitmqVersion = '5.19.0' reactorVersion = '2023.0.2' snappyVersion = '1.1.10.5' - springDataVersion = '2023.1.1' + springDataVersion = '2023.1.2' springRetryVersion = '2.0.5' springVersion = '6.1.2' testcontainersVersion = '1.19.3' From 1a81d06938c80fc3ff352ef72fc6007061a96bf9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Jan 2024 02:22:22 +0000 Subject: [PATCH 361/737] Bump org.springframework:spring-framework-bom from 6.1.2 to 6.1.3 (#2598) Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.1.2 to 6.1.3. - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.1.2...v6.1.3) --- updated-dependencies: - dependency-name: org.springframework:spring-framework-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 780f1aeda2..1246d8b8bb 100644 --- a/build.gradle +++ b/build.gradle @@ -70,7 +70,7 @@ ext { snappyVersion = '1.1.10.5' springDataVersion = '2023.1.2' springRetryVersion = '2.0.5' - springVersion = '6.1.2' + springVersion = '6.1.3' testcontainersVersion = '1.19.3' zstdJniVersion = '1.5.5-11' From 9b69adaa21eb3a4fc1683cd93aab83febd6b4d32 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Jan 2024 02:22:56 +0000 Subject: [PATCH 362/737] Bump io.micrometer:micrometer-tracing-bom from 1.2.1 to 1.2.2 (#2599) Bumps [io.micrometer:micrometer-tracing-bom](https://github.com/micrometer-metrics/tracing) from 1.2.1 to 1.2.2. - [Release notes](https://github.com/micrometer-metrics/tracing/releases) - [Commits](https://github.com/micrometer-metrics/tracing/compare/v1.2.1...v1.2.2) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-tracing-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 1246d8b8bb..df28bed7e5 100644 --- a/build.gradle +++ b/build.gradle @@ -62,7 +62,7 @@ ext { lz4Version = '1.8.0' micrometerDocsVersion = '1.0.2' micrometerVersion = '1.12.1' - micrometerTracingVersion = '1.2.1' + micrometerTracingVersion = '1.2.2' mockitoVersion = '5.6.0' rabbitmqStreamVersion = '0.14.0' rabbitmqVersion = '5.19.0' From 7c945986f82329d6adc23ce8c9b7483ea0301145 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Jan 2024 02:29:36 +0000 Subject: [PATCH 363/737] Bump io.micrometer:micrometer-bom from 1.12.1 to 1.12.2 (#2600) Bumps [io.micrometer:micrometer-bom](https://github.com/micrometer-metrics/micrometer) from 1.12.1 to 1.12.2. - [Release notes](https://github.com/micrometer-metrics/micrometer/releases) - [Commits](https://github.com/micrometer-metrics/micrometer/compare/v1.12.1...v1.12.2) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index df28bed7e5..9309c9d87e 100644 --- a/build.gradle +++ b/build.gradle @@ -61,7 +61,7 @@ ext { logbackVersion = '1.4.14' lz4Version = '1.8.0' micrometerDocsVersion = '1.0.2' - micrometerVersion = '1.12.1' + micrometerVersion = '1.12.2' micrometerTracingVersion = '1.2.2' mockitoVersion = '5.6.0' rabbitmqStreamVersion = '0.14.0' From 37ecdf33d1181ec28e34a378ebc74057e7a38ac4 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 17 Jan 2024 12:36:27 -0500 Subject: [PATCH 364/737] Upgrade to the latest reusable workflows --- .github/workflows/backport-issue.yml | 2 +- .github/workflows/ci-snapshot.yml | 2 +- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/merge-dependabot-pr.yml | 2 +- .github/workflows/pr-build.yml | 2 +- .github/workflows/release.yml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/backport-issue.yml b/.github/workflows/backport-issue.yml index c32d9bf190..c46f500e15 100644 --- a/.github/workflows/backport-issue.yml +++ b/.github/workflows/backport-issue.yml @@ -7,6 +7,6 @@ on: jobs: backport-issue: - uses: spring-io/spring-github-workflows/.github/workflows/spring-backport-issue.yml@v1 + uses: spring-io/spring-github-workflows/.github/workflows/spring-backport-issue.yml@v2 secrets: GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/ci-snapshot.yml b/.github/workflows/ci-snapshot.yml index ca04973710..ac1da95ad1 100644 --- a/.github/workflows/ci-snapshot.yml +++ b/.github/workflows/ci-snapshot.yml @@ -17,7 +17,7 @@ concurrency: jobs: build-snapshot: - uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-snapshot.yml@v1 + uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-snapshot.yml@v2 with: gradleTasks: ${{ github.event_name == 'schedule' && '--rerun-tasks' || '' }} secrets: diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 1771c58265..54d562340b 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -16,4 +16,4 @@ permissions: jobs: dispatch-docs-build: if: github.repository_owner == 'spring-projects' - uses: spring-io/spring-github-workflows/.github/workflows/spring-dispatch-docs-build.yml@main + uses: spring-io/spring-github-workflows/.github/workflows/spring-dispatch-docs-build.yml@v2 diff --git a/.github/workflows/merge-dependabot-pr.yml b/.github/workflows/merge-dependabot-pr.yml index 7dfa29e2e0..2e5a3c261f 100644 --- a/.github/workflows/merge-dependabot-pr.yml +++ b/.github/workflows/merge-dependabot-pr.yml @@ -11,4 +11,4 @@ jobs: merge-dependabot-pr: permissions: write-all - uses: spring-io/spring-github-workflows/.github/workflows/spring-merge-dependabot-pr.yml@v1 \ No newline at end of file + uses: spring-io/spring-github-workflows/.github/workflows/spring-merge-dependabot-pr.yml@v2 \ No newline at end of file diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 945b2f94d9..73821067fc 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -7,4 +7,4 @@ on: jobs: build-pull-request: - uses: spring-io/spring-github-workflows/.github/workflows/spring-gradle-pull-request-build.yml@v1 + uses: spring-io/spring-github-workflows/.github/workflows/spring-gradle-pull-request-build.yml@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aa2cfbcdd9..fc09479e40 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: contents: write issues: write - uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-release.yml@v1 + uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-release.yml@v2 secrets: GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} GRADLE_ENTERPRISE_CACHE_USER: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} From cb15cf595e412f82f98c03ab4c42e8cbf61546f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 20 Jan 2024 02:05:15 +0000 Subject: [PATCH 365/737] Bump the development-dependencies group with 1 update (#2604) Bumps the development-dependencies group with 1 update: [io.spring.ge.conventions](https://github.com/spring-io/gradle-enterprise-conventions). Updates `io.spring.ge.conventions` from 0.0.14 to 0.0.15 - [Release notes](https://github.com/spring-io/gradle-enterprise-conventions/releases) - [Commits](https://github.com/spring-io/gradle-enterprise-conventions/compare/v0.0.14...v0.0.15) --- updated-dependencies: - dependency-name: io.spring.ge.conventions dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index d2eed3f267..23d263506e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,6 @@ plugins { id 'com.gradle.enterprise' version '3.14.1' - id 'io.spring.ge.conventions' version '0.0.14' + id 'io.spring.ge.conventions' version '0.0.15' } rootProject.name = 'spring-amqp-dist' From 966338e95a1e63fddda0b8fef2c95c7597516be2 Mon Sep 17 00:00:00 2001 From: raylax Date: Wed, 17 Jan 2024 11:23:13 -0500 Subject: [PATCH 366/737] GH-2602: Fix `x-delay` header to `Long` Fixes: #2602 * Update deprecated API * Fix code style * Remove deprecated API usage * Some code clean of the affected classes --- .../amqp/core/MessageProperties.java | 82 ++++++++++++++++--- .../amqp/support/SimpleAmqpHeaderMapper.java | 9 +- .../amqp/core/MessagePropertiesTests.java | 9 +- .../support/SimpleAmqpHeaderMapperTests.java | 44 ++++++++-- .../DefaultMessagePropertiesConverter.java | 30 ++++--- .../core/RabbitAdminIntegrationTests.java | 32 ++++---- 6 files changed, 149 insertions(+), 57 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java index eba31fb895..f999acc0a3 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -24,6 +24,8 @@ import java.util.List; import java.util.Map; +import org.springframework.util.Assert; + /** * Message Properties for an AMQP message. * @@ -33,6 +35,7 @@ * @author Dmitry Chernyshov * @author Artem Bilan * @author Csaba Soti + * @author Raylax Grey */ public class MessageProperties implements Serializable { @@ -66,6 +69,12 @@ public class MessageProperties implements Serializable { public static final Integer DEFAULT_PRIORITY = 0; + /** + * The maximum value of x-delay header. + * @since 3.1.2 + */ + public static final long X_DELAY_MAX = 0xffffffffL; + private final Map headers = new HashMap<>(); private Date timestamp; @@ -118,7 +127,7 @@ public class MessageProperties implements Serializable { private String consumerQueue; - private Integer receivedDelay; + private Long receivedDelay; private MessageDeliveryMode receivedDeliveryMode; @@ -352,10 +361,13 @@ public String getReceivedRoutingKey() { * received message contains the delay. * @return the received delay. * @since 1.6 + * @deprecated in favor of {@link #getReceivedDelayLong()} * @see #getDelay() */ + @Deprecated(since = "3.1.2", forRemoval = true) public Integer getReceivedDelay() { - return this.receivedDelay; + Long receivedDelay = getReceivedDelayLong(); + return receivedDelay != null ? Math.toIntExact(receivedDelay) : null; } /** @@ -363,8 +375,32 @@ public Integer getReceivedDelay() { * received message contains the delay. * @param receivedDelay the received delay. * @since 1.6 + * @deprecated in favor of {@link #setReceivedDelayLong(Long)} */ + @Deprecated(since = "3.1.2", forRemoval = true) public void setReceivedDelay(Integer receivedDelay) { + setReceivedDelayLong(receivedDelay != null ? receivedDelay.longValue() : null); + } + + /** + * When a delayed message exchange is used the x-delay header on a + * received message contains the delay. + * @return the received delay. + * @since 3.1.2 + * @see #getDelayLong() + */ + public Long getReceivedDelayLong() { + return this.receivedDelay; + } + + /** + * When a delayed message exchange is used the x-delay header on a + * received message contains the delay. + * @param receivedDelay the received delay. + * @since 3.1.2 + * @see #setDelayLong(Long) + */ + public void setReceivedDelayLong(Long receivedDelay) { this.receivedDelay = receivedDelay; } @@ -434,12 +470,35 @@ public void setConsumerQueue(String consumerQueue) { * The x-delay header (outbound). * @return the delay. * @since 1.6 + * @deprecated in favor of {@link #getDelayLong()} * @see #getReceivedDelay() */ + @Deprecated(since = "3.1.2", forRemoval = true) public Integer getDelay() { + Long delay = getDelayLong(); + return delay != null ? Math.toIntExact(delay) : null; + } + + /** + * Set the x-delay header. + * @param delay the delay. + * @since 1.6 + * @deprecated in favor of {@link #setDelayLong(Long)} + */ + @Deprecated(since = "3.1.2", forRemoval = true) + public void setDelay(Integer delay) { + setDelayLong(delay != null ? delay.longValue() : null); + } + + /** + * Get the x-delay header long value. + * @return the delay. + * @since 3.1.2 + */ + public Long getDelayLong() { Object delay = this.headers.get(X_DELAY); - if (delay instanceof Integer) { - return (Integer) delay; + if (delay instanceof Long) { + return (Long) delay; } else { return null; @@ -447,17 +506,18 @@ public Integer getDelay() { } /** - * Set the x-delay header. + * Set the x-delay header to a long value. * @param delay the delay. - * @since 1.6 + * @since 3.1.2 */ - public void setDelay(Integer delay) { + public void setDelayLong(Long delay) { if (delay == null || delay < 0) { this.headers.remove(X_DELAY); + return; } - else { - this.headers.put(X_DELAY, delay); - } + + Assert.isTrue(delay <= X_DELAY_MAX, "Delay cannot exceed " + X_DELAY_MAX); + this.headers.put(X_DELAY, delay); } public boolean isFinalRetryForMessageWithNoId() { diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/SimpleAmqpHeaderMapper.java b/spring-amqp/src/main/java/org/springframework/amqp/support/SimpleAmqpHeaderMapper.java index 3031d3d87c..51af201fa8 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/SimpleAmqpHeaderMapper.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/SimpleAmqpHeaderMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -48,6 +48,7 @@ * @author Gary Russell * @author Artem Bilan * @author Stephane Nicoll + * @author Raylax Grey * @since 1.4 */ public class SimpleAmqpHeaderMapper extends AbstractHeaderMapper implements AmqpHeaderMapper { @@ -69,8 +70,8 @@ public void fromHeaders(MessageHeaders headers, MessageProperties amqpMessagePro amqpMessageProperties.setCorrelationId((String) correlationId); } javaUtils - .acceptIfNotNull(getHeaderIfAvailable(headers, AmqpHeaders.DELAY, Integer.class), - amqpMessageProperties::setDelay) + .acceptIfNotNull(getHeaderIfAvailable(headers, AmqpHeaders.DELAY, Long.class), + amqpMessageProperties::setDelayLong) .acceptIfNotNull(getHeaderIfAvailable(headers, AmqpHeaders.DELIVERY_MODE, MessageDeliveryMode.class), amqpMessageProperties::setDeliveryMode) .acceptIfNotNull(getHeaderIfAvailable(headers, AmqpHeaders.DELIVERY_TAG, Long.class), @@ -150,7 +151,7 @@ public MessageHeaders toHeaders(MessageProperties amqpMessageProperties) { javaUtils .acceptIfCondition(priority != null && priority > 0, AmqpMessageHeaderAccessor.PRIORITY, priority, putObject) - .acceptIfNotNull(AmqpHeaders.RECEIVED_DELAY, amqpMessageProperties.getReceivedDelay(), putObject) + .acceptIfNotNull(AmqpHeaders.RECEIVED_DELAY, amqpMessageProperties.getReceivedDelayLong(), putObject) .acceptIfHasText(AmqpHeaders.RECEIVED_EXCHANGE, amqpMessageProperties.getReceivedExchange(), putString) .acceptIfHasText(AmqpHeaders.RECEIVED_ROUTING_KEY, amqpMessageProperties.getReceivedRoutingKey(), diff --git a/spring-amqp/src/test/java/org/springframework/amqp/core/MessagePropertiesTests.java b/spring-amqp/src/test/java/org/springframework/amqp/core/MessagePropertiesTests.java index 6a1df9d995..2d080e8ea4 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/core/MessagePropertiesTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/core/MessagePropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -30,6 +30,7 @@ * @author Artem Bilan * @author Gary Russell * @author Csaba Soti + * @author Raylax Grey * */ public class MessagePropertiesTests { @@ -53,10 +54,10 @@ public void testReplyToNullByDefault() { @Test public void testDelayHeader() { MessageProperties properties = new MessageProperties(); - Integer delay = 100; - properties.setDelay(delay); + Long delay = 100L; + properties.setDelayLong(delay); assertThat(properties.getHeaders().get(MessageProperties.X_DELAY)).isEqualTo(delay); - properties.setDelay(null); + properties.setDelayLong(null); assertThat(properties.getHeaders().containsKey(MessageProperties.X_DELAY)).isFalse(); } diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/SimpleAmqpHeaderMapperTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/SimpleAmqpHeaderMapperTests.java index 2e9c768279..2e4d846783 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/support/SimpleAmqpHeaderMapperTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/support/SimpleAmqpHeaderMapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -17,6 +17,7 @@ package org.springframework.amqp.support; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.fail; import java.util.Date; @@ -37,13 +38,14 @@ * @author Mark Fisher * @author Gary Russell * @author Oleg Zhurakousky + * @author Raylax Grey */ public class SimpleAmqpHeaderMapperTests { @Test public void fromHeaders() { SimpleAmqpHeaderMapper headerMapper = new SimpleAmqpHeaderMapper(); - Map headerMap = new HashMap(); + Map headerMap = new HashMap<>(); headerMap.put(AmqpHeaders.APP_ID, "test.appId"); headerMap.put(AmqpHeaders.CLUSTER_ID, "test.clusterId"); headerMap.put(AmqpHeaders.CONTENT_ENCODING, "test.contentEncoding"); @@ -51,7 +53,7 @@ public void fromHeaders() { headerMap.put(AmqpHeaders.CONTENT_TYPE, "test.contentType"); String testCorrelationId = "foo"; headerMap.put(AmqpHeaders.CORRELATION_ID, testCorrelationId); - headerMap.put(AmqpHeaders.DELAY, 1234); + headerMap.put(AmqpHeaders.DELAY, 1234L); headerMap.put(AmqpHeaders.DELIVERY_MODE, MessageDeliveryMode.NON_PERSISTENT); headerMap.put(AmqpHeaders.DELIVERY_TAG, 1234L); headerMap.put(AmqpHeaders.EXPIRATION, "test.expiration"); @@ -92,13 +94,39 @@ public void fromHeaders() { assertThat(amqpProperties.getTimestamp()).isEqualTo(testTimestamp); assertThat(amqpProperties.getType()).isEqualTo("test.type"); assertThat(amqpProperties.getUserId()).isEqualTo("test.userId"); - assertThat(amqpProperties.getDelay()).isEqualTo(Integer.valueOf(1234)); + assertThat(amqpProperties.getDelayLong()).isEqualTo(Long.valueOf(1234)); } + @Test + public void fromHeadersWithLongDelay() { + SimpleAmqpHeaderMapper headerMapper = new SimpleAmqpHeaderMapper(); + Map headerMap = new HashMap<>(); + headerMap.put(AmqpHeaders.DELAY, 1234L); + MessageHeaders messageHeaders = new MessageHeaders(headerMap); + MessageProperties amqpProperties = new MessageProperties(); + headerMapper.fromHeaders(messageHeaders, amqpProperties); + assertThat(amqpProperties.getDelayLong()).isEqualTo(Long.valueOf(1234)); + + amqpProperties.setDelayLong(5678L); + assertThat(amqpProperties.getDelayLong()).isEqualTo(Long.valueOf(5678)); + + amqpProperties.setDelayLong(null); + assertThat(amqpProperties.getHeaders().containsKey(AmqpHeaders.DELAY)).isFalse(); + + amqpProperties.setDelayLong(MessageProperties.X_DELAY_MAX); + assertThat(amqpProperties.getDelayLong()).isEqualTo(Long.valueOf(MessageProperties.X_DELAY_MAX)); + + assertThatThrownBy(() -> amqpProperties.setDelayLong(MessageProperties.X_DELAY_MAX + 1)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Delay cannot exceed"); + + } + + @Test public void fromHeadersWithContentTypeAsMediaType() { SimpleAmqpHeaderMapper headerMapper = new SimpleAmqpHeaderMapper(); - Map headerMap = new HashMap(); + Map headerMap = new HashMap<>(); headerMap.put(AmqpHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_HTML); @@ -126,7 +154,7 @@ public void toHeaders() { amqpProperties.setMessageCount(42); amqpProperties.setMessageId("test.messageId"); amqpProperties.setPriority(22); - amqpProperties.setReceivedDelay(1234); + amqpProperties.setReceivedDelayLong(1234L); amqpProperties.setReceivedExchange("test.receivedExchange"); amqpProperties.setReceivedRoutingKey("test.receivedRoutingKey"); amqpProperties.setRedelivered(true); @@ -151,7 +179,7 @@ public void toHeaders() { assertThat(headerMap.get(AmqpHeaders.EXPIRATION)).isEqualTo("test.expiration"); assertThat(headerMap.get(AmqpHeaders.MESSAGE_COUNT)).isEqualTo(42); assertThat(headerMap.get(AmqpHeaders.MESSAGE_ID)).isEqualTo("test.messageId"); - assertThat(headerMap.get(AmqpHeaders.RECEIVED_DELAY)).isEqualTo(1234); + assertThat(headerMap.get(AmqpHeaders.RECEIVED_DELAY)).isEqualTo(1234L); assertThat(headerMap.get(AmqpHeaders.RECEIVED_EXCHANGE)).isEqualTo("test.receivedExchange"); assertThat(headerMap.get(AmqpHeaders.RECEIVED_ROUTING_KEY)).isEqualTo("test.receivedRoutingKey"); assertThat(headerMap.get(AmqpHeaders.REPLY_TO)).isEqualTo("test.replyTo"); @@ -170,7 +198,7 @@ public void jsonTypeIdNotOverwritten() { Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter(); MessageProperties amqpProperties = new MessageProperties(); converter.toMessage("123", amqpProperties); - Map headerMap = new HashMap(); + Map headerMap = new HashMap<>(); headerMap.put("__TypeId__", "java.lang.Integer"); MessageHeaders messageHeaders = new MessageHeaders(headerMap); headerMapper.fromHeaders(messageHeaders, amqpProperties); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java index 72053d2846..a90fae67c1 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -40,6 +40,7 @@ * @author Mark Fisher * @author Gary Russell * @author Soeren Unruh + * @author Raylax Grey * @since 1.0 */ public class DefaultMessagePropertiesConverter implements MessagePropertiesConverter { @@ -83,8 +84,7 @@ public DefaultMessagePropertiesConverter(int longStringLimit, boolean convertLon } @Override - public MessageProperties toMessageProperties(final BasicProperties source, final Envelope envelope, - final String charset) { + public MessageProperties toMessageProperties(BasicProperties source, @Nullable Envelope envelope, String charset) { MessageProperties target = new MessageProperties(); Map headers = source.getHeaders(); if (!CollectionUtils.isEmpty(headers)) { @@ -92,8 +92,14 @@ public MessageProperties toMessageProperties(final BasicProperties source, final String key = entry.getKey(); if (MessageProperties.X_DELAY.equals(key)) { Object value = entry.getValue(); - if (value instanceof Integer integ) { - target.setReceivedDelay(integ); + if (value instanceof Integer intValue) { + long receivedDelayLongValue = intValue.longValue(); + target.setReceivedDelayLong(receivedDelayLongValue); + target.setHeader(key, receivedDelayLongValue); + } + else if (value instanceof Long longVal) { + target.setReceivedDelayLong(longVal); + target.setHeader(key, longVal); } } else { @@ -164,9 +170,9 @@ public BasicProperties fromMessageProperties(final MessageProperties source, fin private Map convertHeadersIfNecessary(Map headers) { if (CollectionUtils.isEmpty(headers)) { - return Collections.emptyMap(); + return Collections.emptyMap(); } - Map writableHeaders = new HashMap(); + Map writableHeaders = new HashMap<>(); for (Map.Entry entry : headers.entrySet()) { writableHeaders.put(entry.getKey(), this.convertHeaderValueIfNecessary(entry.getValue())); } @@ -200,7 +206,7 @@ else if (value instanceof Object[] array) { value = writableArray; } else if (value instanceof List) { - List writableList = new ArrayList(((List) value).size()); + List writableList = new ArrayList<>(((List) value).size()); for (Object listValue : (List) value) { writableList.add(convertHeaderValueIfNecessary(listValue)); } @@ -209,7 +215,7 @@ else if (value instanceof List) { else if (value instanceof Map) { @SuppressWarnings("unchecked") Map originalMap = (Map) value; - Map writableMap = new HashMap(originalMap.size()); + Map writableMap = new HashMap<>(originalMap.size()); for (Map.Entry entry : originalMap.entrySet()) { writableMap.put(entry.getKey(), this.convertHeaderValueIfNecessary(entry.getValue())); } @@ -222,7 +228,7 @@ else if (value instanceof Class clazz) { } /** - * Converts a LongString value to either a String or DataInputStream based on a + * Convert a LongString value to either a String or DataInputStream based on a * length-driven threshold. If the length is {@link #longStringLimit} bytes or less, a * String will be returned, otherwise a DataInputStream is returned or the {@link LongString} * is returned unconverted if {@link #convertLongLongStrings} is true. @@ -257,7 +263,7 @@ private Object convertLongStringIfNecessary(Object valueArg, String charset) { value = convertLongString(longStr, charset); } else if (value instanceof List) { - List convertedList = new ArrayList(((List) value).size()); + List convertedList = new ArrayList<>(((List) value).size()); for (Object listValue : (List) value) { convertedList.add(this.convertLongStringIfNecessary(listValue, charset)); } @@ -266,7 +272,7 @@ else if (value instanceof List) { else if (value instanceof Map) { @SuppressWarnings("unchecked") Map originalMap = (Map) value; - Map convertedMap = new HashMap(); + Map convertedMap = new HashMap<>(); for (Map.Entry entry : originalMap.entrySet()) { convertedMap.put(entry.getKey(), this.convertLongStringIfNecessary(entry.getValue(), charset)); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminIntegrationTests.java index abf8f76cb3..aa2f95f648 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -17,12 +17,14 @@ package org.springframework.amqp.rabbit.core; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.awaitility.Awaitility.await; import java.io.IOException; import java.time.Duration; import java.util.Map; +import java.util.Objects; import java.util.UUID; import org.junit.jupiter.api.AfterEach; @@ -59,6 +61,7 @@ * @author Gary Russell * @author Gunnar Hillert * @author Artem Bilan + * @author Raylax Grey */ @RabbitAvailable(management = true) public class RabbitAdminIntegrationTests extends NeedsManagementTests { @@ -130,7 +133,7 @@ public void testDoubleDeclarationOfExclusiveQueue() { @Test public void testDoubleDeclarationOfAutodeleteQueue() { - // No error expected here: the queue is autodeleted when the last consumer is cancelled, but this one never has + // No error expected here: the queue is auto-deleted when the last consumer is cancelled, but this one never has // any consumers. CachingConnectionFactory connectionFactory1 = new CachingConnectionFactory(); connectionFactory1.setHost("localhost"); @@ -236,9 +239,7 @@ public void testSpringWithDefaultExchange() { context.getBeanFactory().registerSingleton("foo", exchange); rabbitAdmin.afterPropertiesSet(); - rabbitAdmin.initialize(); - - // Pass by virtue of RabbitMQ not firing a 403 reply code + assertThatNoException().isThrownBy(rabbitAdmin::initialize); } @Test @@ -367,7 +368,6 @@ public void testQueueDeclareBad() { this.rabbitAdmin.deleteQueue(queue.getName()); } - @SuppressWarnings("unchecked") @Test public void testDeclareDelayedExchange() throws Exception { DirectExchange exchange = new DirectExchange("test.delayed.exchange"); @@ -397,20 +397,20 @@ public void testDeclareDelayedExchange() throws Exception { RabbitTemplate template = new RabbitTemplate(this.connectionFactory); template.setReceiveTimeout(10000); template.convertAndSend(exchangeName, queue.getName(), "foo", message -> { - message.getMessageProperties().setDelay(1000); + message.getMessageProperties().setDelayLong(1000L); return message; }); MessageProperties properties = new MessageProperties(); - properties.setDelay(500); + properties.setDelayLong(500L); template.send(exchangeName, queue.getName(), MessageBuilder.withBody("foo".getBytes()).andProperties(properties).build()); long t1 = System.currentTimeMillis(); Message received = template.receive(queue.getName()); assertThat(received).isNotNull(); - assertThat(received.getMessageProperties().getReceivedDelay()).isEqualTo(Integer.valueOf(500)); + assertThat(received.getMessageProperties().getDelayLong()).isEqualTo(500L); received = template.receive(queue.getName()); assertThat(received).isNotNull(); - assertThat(received.getMessageProperties().getReceivedDelay()).isEqualTo(Integer.valueOf(1000)); + assertThat(received.getMessageProperties().getDelayLong()).isEqualTo(1000L); assertThat(System.currentTimeMillis() - t1).isGreaterThan(950L); Map exchange2 = getExchange(exchangeName); @@ -421,9 +421,9 @@ public void testDeclareDelayedExchange() throws Exception { this.rabbitAdmin.deleteExchange(exchangeName); } - private Map getExchange(String exchangeName) throws Exception { + private Map getExchange(String exchangeName) { return await().pollDelay(Duration.ZERO) - .until(() -> exchangeInfo(exchangeName), exch -> exch != null); + .until(() -> exchangeInfo(exchangeName), Objects::nonNull); } /** @@ -437,18 +437,14 @@ private boolean queueExists(final Queue queue) throws Exception { ConnectionFactory cf = new ConnectionFactory(); cf.setHost("localhost"); cf.setPort(BrokerTestUtils.getPort()); - Connection connection = cf.newConnection(); - Channel channel = connection.createChannel(); - try { + try (Connection connection = cf.newConnection()) { + Channel channel = connection.createChannel(); DeclareOk result = channel.queueDeclarePassive(queue.getName()); return result != null; } catch (IOException e) { return e.getCause().getMessage().contains("RESOURCE_LOCKED"); } - finally { - connection.close(); - } } } From 729b405f5e0ade600d105f2095943d408f414630 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 22 Jan 2024 14:15:01 -0500 Subject: [PATCH 367/737] Fix broken links in docs --- .../ROOT/pages/amqp/broker-configuration.adoc | 2 +- .../modules/ROOT/pages/amqp/connections.adoc | 2 +- .../ROOT/pages/amqp/containerAttributes.adoc | 16 ++++++++-------- .../modules/ROOT/pages/amqp/multi-rabbit.adoc | 2 +- .../container-management.adoc | 2 +- .../async-annotation-driven/conversion.adoc | 2 +- .../amqp/receiving-messages/async-consumer.adoc | 2 +- .../amqp/receiving-messages/consumer-events.adoc | 2 +- .../pages/amqp/receiving-messages/threading.adoc | 2 +- .../antora/modules/ROOT/pages/amqp/template.adoc | 4 ++-- 10 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc b/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc index 6de2afaa41..bd0160f618 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc @@ -45,7 +45,7 @@ See also xref:amqp/template.adoc#scoped-operations[Scoped Operations]. The `getQueueProperties()` method returns some limited information about the queue (message count and consumer count). The keys for the properties returned are available as constants in the `RabbitTemplate` (`QUEUE_NAME`, `QUEUE_MESSAGE_COUNT`, and `QUEUE_CONSUMER_COUNT`). -The <> provides much more information in the `QueueInfo` object. +The xref:amqp/management-rest-api.adoc#management-rest-api[RabbitMQ REST API] provides much more information in the `QueueInfo` object. The no-arg `declareQueue()` method defines a queue on the broker with a name that is automatically generated. The additional properties of this auto-generated queue are `exclusive=true`, `autoDelete=true`, and `durable=false`. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc b/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc index a8bf47327f..44abca5502 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc @@ -543,7 +543,7 @@ public RabbitTemplate rabbitTemplate() { } ---- -This way messages with the header `x-use-publisher-confirms: true` will be sent through the caching connection and you can ensure the message delivery. +This way messages with the header `x-use-publisher-confirms: true` will be sent through the caching connection, and you can ensure the message delivery. See xref:amqp/connections.adoc#cf-pub-conf-ret[Publisher Confirms and Returns] for more information about ensuring message delivery. [[queue-affinity]] diff --git a/src/reference/antora/modules/ROOT/pages/amqp/containerAttributes.adoc b/src/reference/antora/modules/ROOT/pages/amqp/containerAttributes.adoc index 5d63f226d0..4f9d827f33 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/containerAttributes.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/containerAttributes.adoc @@ -144,7 +144,7 @@ a| |`m-n` The range of concurrent consumers for each listener (min, max). If only `n` is provided, `n` is a fixed number of consumers. -See <>. +See xref:amqp/listener-concurrency.adoc#listener-concurrency[Listener Concurrency]. a|image::tickmark.png[] a| @@ -154,7 +154,7 @@ a| (concurrency) |The number of concurrent consumers to initially start for each listener. -See <>. +See xref:amqp/listener-concurrency.adoc#listener-concurrency[Listener Concurrency]. For the `StLC`, concurrency is controlled via an overloaded `superStream` method; see xref:stream.adoc#super-stream-consumer[Consuming Super Streams with Single Active Consumers]. a|image::tickmark.png[] @@ -176,7 +176,7 @@ a| |The minimum number of consecutive messages received by a consumer, without a receive timeout occurring, when considering starting a new consumer. Also impacted by 'batchSize'. -See <>. +See xref:amqp/listener-concurrency.adoc#listener-concurrency[Listener Concurrency]. Default: 10. a|image::tickmark.png[] @@ -188,7 +188,7 @@ a| |The minimum number of receive timeouts a consumer must experience before considering stopping a consumer. Also impacted by 'batchSize'. -See <>. +See xref:amqp/listener-concurrency.adoc#listener-concurrency[Listener Concurrency]. Default: 10. a|image::tickmark.png[] @@ -241,7 +241,7 @@ a| (consumers-per-queue) |The number of consumers to create for each configured queue. -See <>. +See xref:amqp/listener-concurrency.adoc#listener-concurrency[Listener Concurrency]. a| a|image::tickmark.png[] @@ -409,7 +409,7 @@ a| |The maximum number of concurrent consumers to start, if needed, on demand. Must be greater than or equal to 'concurrentConsumers'. -See <>. +See xref:amqp/listener-concurrency.adoc#listener-concurrency[Listener Concurrency]. a|image::tickmark.png[] a| @@ -663,7 +663,7 @@ a| (min-start-interval) |The time in milliseconds that must elapse before each new consumer is started on demand. -See <>. +See xref:amqp/listener-concurrency.adoc#listener-concurrency[Listener Concurrency]. Default: 10000 (10 seconds). a|image::tickmark.png[] @@ -686,7 +686,7 @@ a| (min-stop-interval) |The time in milliseconds that must elapse before a consumer is stopped since the last consumer was stopped when an idle consumer is detected. -See <>. +See xref:amqp/listener-concurrency.adoc#listener-concurrency[Listener Concurrency]. Default: 60000 (one minute). a|image::tickmark.png[] diff --git a/src/reference/antora/modules/ROOT/pages/amqp/multi-rabbit.adoc b/src/reference/antora/modules/ROOT/pages/amqp/multi-rabbit.adoc index 9d1a97d940..420fa05d44 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/multi-rabbit.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/multi-rabbit.adoc @@ -129,7 +129,7 @@ By naming the `RabbitAdmin` beans with the convention `- This will also work with `bindings = @QueueBinding(...)` whereby the exchange and binding will also be declared. It will NOT work with `queues`, since that expects the queue(s) to already exist. -On the producer side, a convenient `ConnectionFactoryContextWrapper` class is provided, to make using the `RoutingConnectionFactory` (see <>) simpler. +On the producer side, a convenient `ConnectionFactoryContextWrapper` class is provided, to make using the `RoutingConnectionFactory` (see xref:amqp/connections.adoc#routing-connection-factory[Routing Connection Factory]) simpler. As you can see above, a `SimpleRoutingConnectionFactory` bean has been added with routing keys `one`, `two` and `three`. There is also a `RabbitTemplate` that uses that factory. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/container-management.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/container-management.adoc index 2b4a69c857..b9d71cc174 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/container-management.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/container-management.adoc @@ -18,6 +18,6 @@ This provides a mechanism to get a reference to a subset of containers. Adding a `group` attribute causes a bean of type `Collection` to be registered with the context with the group name. By default, stopping a container will cancel the consumer and process all prefetched messages before stopping. -Starting with versions 2.4.14, 3.0.6, you can set the <> container property to true to stop immediately after the current message is processed, causing any prefetched messages to be requeued. +Starting with versions 2.4.14, 3.0.6, you can set the xref:amqp/containerAttributes.adoc#forceStop[`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. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/conversion.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/conversion.adoc index 8f2adbe6de..cd18a60185 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/conversion.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/conversion.adoc @@ -60,7 +60,7 @@ method arguments. NOTE: This type inference works only for `@RabbitListener` at the method level. -See <> for more information. +See xref:amqp/message-converters.adoc#json-message-converter[`Jackson2JsonMessageConverter`] for more information. If you wish to customize the method argument converter, you can do so as follows: diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-consumer.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-consumer.adoc index 5c11273f91..c7b0f33374 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-consumer.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-consumer.adoc @@ -237,7 +237,7 @@ For convenience, the namespace provides the `priority` attribute on the `listene ---- Starting with version 1.3, you can modify the queues on which the container listens at runtime. -See <>. +See xref:amqp/listener-queues.adoc#listener-queues[Listener Container Queues]. [[lc-auto-delete]] == `auto-delete` Queues diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/consumer-events.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/consumer-events.adoc index af5e756f94..fe6bed849c 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/consumer-events.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/consumer-events.adoc @@ -22,7 +22,7 @@ A new method `logRestart()` has been added to the `ConditionalExceptionLogger` t Also, the `AbstractMessageListenerContainer.DefaultExclusiveConsumerLogger` is now public, allowing it to be sub classed. -See also <>. +See also xref:amqp/connections.adoc#channel-close-logging[Logging Channel Close Events]. Fatal errors are always logged at the `ERROR` level. This it not modifiable. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/threading.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/threading.adoc index 0edb275b17..b19c58bfb0 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/threading.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/threading.adoc @@ -24,5 +24,5 @@ IMPORTANT: With the `DirectMessageListenerContainer`, you need to ensure that th The default pool size (at the time of writing) is `Runtime.getRuntime().availableProcessors() * 2`. 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`, as discussed in <>. +To modify this factory, you need to configure the underlying RabbitMQ `ConnectionFactory`, as discussed in xref:amqp/connections.adoc#connection-factory[Configuring the Underlying Client Connection Factory]. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/template.adoc b/src/reference/antora/modules/ROOT/pages/amqp/template.adoc index 2973335b86..55b17e8cb3 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/template.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/template.adoc @@ -12,8 +12,8 @@ Currently, there is only a single implementation: `RabbitTemplate`. In the examples that follow, we often use an `AmqpTemplate`. However, when you look at the configuration examples or any code excerpts where the template is instantiated or setters are invoked, you can see the implementation type (for example, `RabbitTemplate`). -As mentioned earlier, the `AmqpTemplate` interface defines all of the basic operations for sending and receiving messages. -We will explore message sending and reception, respectively, in <> and <>. +As mentioned earlier, the `AmqpTemplate` interface defines all the basic operations for sending and receiving messages. +We will explore message sending and reception, respectively, in xref:amqp/sending-messages.adoc#sending-messages[Sending Messages] and xref:amqp/receiving-messages.adoc#receiving-messages[Receiving Messages]. See also xref:amqp/request-reply.adoc#async-template[Async Rabbit Template]. From 7ad35b57463f4738ef61d339d8fd18846cf38bbc Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 23 Jan 2024 11:06:22 -0500 Subject: [PATCH 368/737] Change budge to Develocity --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1cd970efa6..090a834c1c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ Spring AMQP [![Build Status](https://github.com/spring-projects/spring-amqp/actions/workflows/ci-snapshot.yml/badge.svg)](https://github.com/spring-projects/spring-amqp/actions/workflows/ci-snapshot.yml) -[![Revved up by Gradle Enterprise](https://img.shields.io/badge/Revved%20up%20by-Gradle%20Enterprise-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.spring.io/scans?search.rootProjectNames=spring-amqp) +[![Revved up by Develocity](https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.spring.io/scans?search.rootProjectNames=spring-amqp) =========== This project provides support for using Spring and Java with [AMQP 0.9.1](https://www.rabbitmq.com/amqp-0-9-1-reference.html), and in particular [RabbitMQ](https://www.rabbitmq.com/). From 7166859fc0bd40d17aea3dfbdf4258a37272f31b Mon Sep 17 00:00:00 2001 From: laststem Date: Fri, 26 Jan 2024 00:52:13 +0900 Subject: [PATCH 369/737] GH-2601: Add a batchReceiveTimeout (#2605) Fixes: #2601 Stop to waiting next message and execute listener when `batchReceiveTimeout` is timed out. * Add `batchReceiveTimeout` to the `SimpleMessageListenerContainer` configuration. --- .../config/ListenerContainerFactoryBean.java | 18 ++++++- .../SimpleRabbitListenerContainerFactory.java | 21 +++++++- .../SimpleMessageListenerContainer.java | 30 ++++++++++- .../SimpleMessageListenerContainerTests.java | 54 +++++++++++++++++++ .../ROOT/pages/amqp/containerAttributes.adoc | 14 ++++- 5 files changed, 131 insertions(+), 6 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java index a23d29ca83..0a070e1eca 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 the original author or authors. + * Copyright 2016-2024 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. @@ -55,6 +55,7 @@ * @author Gary Russell * @author Artem Bilan * @author Johno Crawford + * @author Jeonggi Kim * * @since 2.0 * @@ -166,6 +167,8 @@ public class ListenerContainerFactoryBean extends AbstractFactoryBean @@ -552,6 +567,7 @@ private AbstractMessageListenerContainer createContainer() { .acceptIfNotNull(this.consecutiveActiveTrigger, container::setConsecutiveActiveTrigger) .acceptIfNotNull(this.consecutiveIdleTrigger, container::setConsecutiveIdleTrigger) .acceptIfNotNull(this.receiveTimeout, container::setReceiveTimeout) + .acceptIfNotNull(this.batchReceiveTimeout, container::setBatchReceiveTimeout) .acceptIfNotNull(this.batchSize, container::setBatchSize) .acceptIfNotNull(this.consumerBatchEnabled, container::setConsumerBatchEnabled) .acceptIfNotNull(this.declarationRetries, container::setDeclarationRetries) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerContainerFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerContainerFactory.java index 2e8c5afdd6..baa4c975df 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerContainerFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerContainerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2024 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. @@ -32,6 +32,7 @@ * @author Gary Russell * @author Artem Bilan * @author Dustin Schultz + * @author Jeonggi Kim * * @since 1.4 */ @@ -54,6 +55,8 @@ public class SimpleRabbitListenerContainerFactory private Long receiveTimeout; + private Long batchReceiveTimeout; + private Boolean consumerBatchEnabled; /** @@ -121,6 +124,19 @@ public void setReceiveTimeout(Long receiveTimeout) { this.receiveTimeout = receiveTimeout; } + /** + * The number of milliseconds of timeout for gathering batch messages. + * It limits the time to wait to fill batchSize. + * Default is 0 (no timeout). + * @param batchReceiveTimeout the timeout for gathering batch messages. + * @since 3.1.2 + * @see SimpleMessageListenerContainer#setBatchReceiveTimeout + * @see #setBatchSize(Integer) + */ + public void setBatchReceiveTimeout(Long batchReceiveTimeout) { + this.batchReceiveTimeout = batchReceiveTimeout; + } + /** * Set to true to present a list of messages based on the {@link #setBatchSize(Integer)}, * if the listener supports it. Starting with version 3.0, setting this to true will @@ -163,7 +179,8 @@ protected void initializeContainer(SimpleMessageListenerContainer instance, Rabb .acceptIfNotNull(this.stopConsumerMinInterval, instance::setStopConsumerMinInterval) .acceptIfNotNull(this.consecutiveActiveTrigger, instance::setConsecutiveActiveTrigger) .acceptIfNotNull(this.consecutiveIdleTrigger, instance::setConsecutiveIdleTrigger) - .acceptIfNotNull(this.receiveTimeout, instance::setReceiveTimeout); + .acceptIfNotNull(this.receiveTimeout, instance::setReceiveTimeout) + .acceptIfNotNull(this.batchReceiveTimeout, instance::setBatchReceiveTimeout); if (Boolean.TRUE.equals(this.consumerBatchEnabled)) { instance.setConsumerBatchEnabled(true); /* diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java index 9007bfda0d..737ee14716 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -80,6 +80,7 @@ * @author Mat Jaggard * @author Yansong Ren * @author Tim Bourquin + * @author Jeonggi Kim * * @since 1.0 */ @@ -121,6 +122,8 @@ public class SimpleMessageListenerContainer extends AbstractMessageListenerConta private long receiveTimeout = DEFAULT_RECEIVE_TIMEOUT; + private long batchReceiveTimeout; + private Set consumers; private Integer declarationRetries; @@ -330,6 +333,19 @@ public void setReceiveTimeout(long receiveTimeout) { this.receiveTimeout = receiveTimeout; } + /** + * The number of milliseconds of timeout for gathering batch messages. + * It limits the time to wait to fill batchSize. + * Default is 0 (no timeout). + * @param batchReceiveTimeout the timeout for gathering batch messages. + * @since 3.1.2 + * @see #setBatchSize(int) + */ + public void setBatchReceiveTimeout(long batchReceiveTimeout) { + Assert.isTrue(batchReceiveTimeout >= 0, "'batchReceiveTimeout' must be >= 0"); + this.batchReceiveTimeout = batchReceiveTimeout; + } + /** * This property has several functions. *

@@ -996,8 +1012,18 @@ private boolean doReceiveAndExecute(BlockingQueueConsumer consumer) throws Excep List messages = null; long deliveryTag = 0; - + boolean isBatchReceiveTimeoutEnabled = this.batchReceiveTimeout > 0; + long startTime = isBatchReceiveTimeoutEnabled ? System.currentTimeMillis() : 0; for (int i = 0; i < this.batchSize; i++) { + boolean batchTimedOut = isBatchReceiveTimeoutEnabled && + (System.currentTimeMillis() - startTime) > this.batchReceiveTimeout; + if (batchTimedOut) { + if (logger.isTraceEnabled()) { + long gathered = messages != null ? messages.size() : 0; + logger.trace("Timed out for gathering batch messages. gathered size is " + gathered); + } + break; + } logger.trace("Waiting for message from consumer."); Message message = consumer.nextMessage(this.receiveTimeout); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java index 7044f5ee6c..26c72b14e3 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java @@ -110,6 +110,7 @@ * @author Mohammad Hewedy * @author Yansong Ren * @author Tim Bourquin + * @author Jeonggi Kim */ public class SimpleMessageListenerContainerTests { @@ -784,6 +785,59 @@ void testWithConsumerStartWhenNotActive() { assertThat(start.getCount()).isEqualTo(0L); } + @Test + public void testBatchReceiveTimedOut() throws Exception { + ConnectionFactory connectionFactory = mock(ConnectionFactory.class); + Connection connection = mock(Connection.class); + Channel channel = mock(Channel.class); + given(connectionFactory.createConnection()).willReturn(connection); + given(connection.createChannel(false)).willReturn(channel); + final AtomicReference consumer = new AtomicReference<>(); + willAnswer(invocation -> { + consumer.set(invocation.getArgument(6)); + consumer.get().handleConsumeOk("1"); + return "1"; + }).given(channel) + .basicConsume(anyString(), anyBoolean(), anyString(), anyBoolean(), anyBoolean(), anyMap(), + any(Consumer.class)); + final CountDownLatch latch = new CountDownLatch(2); + willAnswer(invocation -> { + latch.countDown(); + return null; + }).given(channel).basicAck(anyLong(), anyBoolean()); + + final SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); + container.setAfterReceivePostProcessors(msg -> null); + container.setQueueNames("foo"); + MessageListener listener = mock(BatchMessageListener.class); + container.setMessageListener(listener); + container.setBatchSize(3); + container.setConsumerBatchEnabled(true); + container.setReceiveTimeout(10); + container.setBatchReceiveTimeout(20); + container.start(); + + BasicProperties props = new BasicProperties(); + byte[] payload = "baz".getBytes(); + Envelope envelope = new Envelope(1L, false, "foo", "bar"); + consumer.get().handleDelivery("1", envelope, props, payload); + envelope = new Envelope(2L, false, "foo", "bar"); + consumer.get().handleDelivery("1", envelope, props, payload); + // waiting for batch receive timed out + Thread.sleep(20); + envelope = new Envelope(3L, false, "foo", "bar"); + consumer.get().handleDelivery("1", envelope, props, payload); + assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); + verify(channel, never()).basicAck(eq(1), anyBoolean()); + verify(channel).basicAck(2, true); + verify(channel, never()).basicAck(eq(2), anyBoolean()); + verify(channel).basicAck(3, true); + container.stop(); + verify(listener).containerAckMode(AcknowledgeMode.AUTO); + verify(listener).isAsyncReplies(); + verifyNoMoreInteractions(listener); + } + private Answer messageToConsumer(final Channel mockChannel, final SimpleMessageListenerContainer container, final boolean cancel, final CountDownLatch latch) { return invocation -> { diff --git a/src/reference/antora/modules/ROOT/pages/amqp/containerAttributes.adoc b/src/reference/antora/modules/ROOT/pages/amqp/containerAttributes.adoc index 4f9d827f33..9aec7d7c9b 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/containerAttributes.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/containerAttributes.adoc @@ -198,7 +198,7 @@ a| |[[consumerBatchEnabled]]<> + (batch-enabled) -|If the `MessageListener` supports it, setting this to true enables batching of discrete messages, up to `batchSize`; a partial batch will be delivered if no new messages arrive in `receiveTimeout`. +|If the `MessageListener` supports it, setting this to true enables batching of discrete messages, up to `batchSize`; a partial batch will be delivered if no new messages arrive in `receiveTimeout` or gathering batch messages time exceeded `batchReceiveTimeout`. When this is false, batching is only supported for batches created by a producer; see xref:amqp/sending-messages.adoc#template-batching[Batching]. a|image::tickmark.png[] @@ -611,6 +611,18 @@ a|image::tickmark.png[] a| a| +|[[batchReceiveTimeout]]<> + +(batch-receive-timeout) + +|The number of milliseconds of timeout for gathering batch messages. +It limits the time to wait to fill batchSize. +When `batchSize > 1` and the time to gathering batch messages is greater than `batchReceiveTime`, batch will be delivered. +Default is 0 (no timeout). + +a|image::tickmark.png[] +a| +a| + |[[recoveryBackOff]]<> + (recovery-back-off) From 01102145dd812189a90b980eff632f97c7d730fa Mon Sep 17 00:00:00 2001 From: Soby Chacko Date: Fri, 26 Jan 2024 16:30:57 -0500 Subject: [PATCH 370/737] Fix 404 issue with the reference docs Due to an antora ordering issue, the reference docs on the latest GA from the project site throws an HTTP 404. See spring-projects/spring-framework#32083 for more details. --- src/reference/antora/antora-playbook.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/reference/antora/antora-playbook.yml b/src/reference/antora/antora-playbook.yml index 999864f16b..3d0e26ceb7 100644 --- a/src/reference/antora/antora-playbook.yml +++ b/src/reference/antora/antora-playbook.yml @@ -1,10 +1,11 @@ antora: extensions: - '@springio/antora-extensions/partial-build-extension' + - # atlas-extension must be before latest-version-extension so the latest versions are applied to imported versions + - '@antora/atlas-extension' - require: '@springio/antora-extensions/latest-version-extension' - require: '@springio/antora-extensions/inject-collector-cache-config-extension' - '@antora/collector-extension' - - '@antora/atlas-extension' - require: '@springio/antora-extensions/root-component-extension' root_component_name: 'amqp' # FIXME: Run antora once using this extension to migrate to the Asciidoc Tabs syntax From 5c3e739c9c791c50ee4f6b52ac206af059d5259f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 27 Jan 2024 02:24:11 +0000 Subject: [PATCH 371/737] Bump org.testcontainers:testcontainers-bom from 1.19.3 to 1.19.4 (#2606) Bumps [org.testcontainers:testcontainers-bom](https://github.com/testcontainers/testcontainers-java) from 1.19.3 to 1.19.4. - [Release notes](https://github.com/testcontainers/testcontainers-java/releases) - [Changelog](https://github.com/testcontainers/testcontainers-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/testcontainers/testcontainers-java/compare/1.19.3...1.19.4) --- updated-dependencies: - dependency-name: org.testcontainers:testcontainers-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 9309c9d87e..60045fec27 100644 --- a/build.gradle +++ b/build.gradle @@ -71,7 +71,7 @@ ext { springDataVersion = '2023.1.2' springRetryVersion = '2.0.5' springVersion = '6.1.3' - testcontainersVersion = '1.19.3' + testcontainersVersion = '1.19.4' zstdJniVersion = '1.5.5-11' javaProjects = subprojects - project(':spring-amqp-bom') From c8c06eacc7b15dfcfc1ead5d175daa97cdc604f8 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 2 Feb 2024 13:50:59 -0500 Subject: [PATCH 372/737] GH-2607: Add `SMLC.enforceImmediateAckForManual` option Fixes: #2607 There are use-cases when `ImmediateAcknowledgeAmqpException` can be thrown outside the listener method, therefore there is no way to reach `Channel.basicAck()`. For example, for `AbstractMessageListenerContainer.afterReceivePostProcessors` * Make force ack for `ImmediateAcknowledgeAmqpException` even if `AcknowledgeMode.MANUAL`. This is controlled with newly introduced `enforceImmediateAckForManual` flag on the `SimpleMessageListenerContainer`. Such an option might be as tentative solution to not break behavior for existing applications using the current point release. We may consider to make this unconditional in future versions --- .../SimpleRabbitListenerContainerFactory.java | 19 +++++++- .../listener/BlockingQueueConsumer.java | 24 +++++++--- .../SimpleMessageListenerContainer.java | 35 ++++++++++---- ...sageListenerManualAckIntegrationTests.java | 47 ++++++++++++++++++- 4 files changed, 108 insertions(+), 17 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerContainerFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerContainerFactory.java index baa4c975df..e0aebfffcc 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerContainerFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerContainerFactory.java @@ -16,6 +16,9 @@ package org.springframework.amqp.rabbit.config; +import com.rabbitmq.client.Channel; + +import org.springframework.amqp.ImmediateAcknowledgeAmqpException; import org.springframework.amqp.rabbit.listener.RabbitListenerEndpoint; import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; import org.springframework.amqp.utils.JavaUtils; @@ -59,6 +62,8 @@ public class SimpleRabbitListenerContainerFactory private Boolean consumerBatchEnabled; + private Boolean enforceImmediateAckForManual; + /** * @param batchSize the batch size. * @since 2.2 @@ -153,6 +158,17 @@ public void setConsumerBatchEnabled(boolean consumerBatchEnabled) { } } + /** + * Set to {@code true} to enforce {@link Channel#basicAck(long, boolean)} + * for {@link org.springframework.amqp.core.AcknowledgeMode#MANUAL} + * when {@link ImmediateAcknowledgeAmqpException} is thrown. + * This might be a tentative solution to not break behavior for current minor version. + * @param enforceImmediateAckForManual the flag to ack message for MANUAL mode on ImmediateAcknowledgeAmqpException + * @since 3.1.2 + */ + public void setEnforceImmediateAckForManual(Boolean enforceImmediateAckForManual) { + this.enforceImmediateAckForManual = enforceImmediateAckForManual; + } @Override protected SimpleMessageListenerContainer createContainerInstance() { return new SimpleMessageListenerContainer(); @@ -180,7 +196,8 @@ protected void initializeContainer(SimpleMessageListenerContainer instance, Rabb .acceptIfNotNull(this.consecutiveActiveTrigger, instance::setConsecutiveActiveTrigger) .acceptIfNotNull(this.consecutiveIdleTrigger, instance::setConsecutiveIdleTrigger) .acceptIfNotNull(this.receiveTimeout, instance::setReceiveTimeout) - .acceptIfNotNull(this.batchReceiveTimeout, instance::setBatchReceiveTimeout); + .acceptIfNotNull(this.batchReceiveTimeout, instance::setBatchReceiveTimeout) + .acceptIfNotNull(this.enforceImmediateAckForManual, instance::setEnforceImmediateAckForManual); if (Boolean.TRUE.equals(this.consumerBatchEnabled)) { instance.setConsumerBatchEnabled(true); /* diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java index de2d5da756..f78a3fe0e0 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java @@ -875,10 +875,25 @@ public void rollbackOnExceptionIfNecessary(Throwable ex, long tag) { /** * Perform a commit or message acknowledgement, as appropriate. + * NOTE: This method was never been intended tobe public. * @param localTx Whether the channel is locally transacted. * @return true if at least one delivery tag exists. + * @deprecated in favor of {@link #commitIfNecessary(boolean, boolean)} */ + @Deprecated(forRemoval = true, since = "3.1.2") public boolean commitIfNecessary(boolean localTx) { + return commitIfNecessary(localTx, false); + } + + /** + * Perform a commit or message acknowledgement, as appropriate. + * NOTE: This method was never been intended tobe public. + * @param localTx Whether the channel is locally transacted. + * @param forceAck perform {@link Channel#basicAck(long, boolean)} independently of {@link #acknowledgeMode}. + * @return true if at least one delivery tag exists. + * @since 3.1.2 + */ + boolean commitIfNecessary(boolean localTx, boolean forceAck) { if (this.deliveryTags.isEmpty()) { return false; } @@ -890,11 +905,10 @@ public boolean commitIfNecessary(boolean localTx) { || (this.transactional && TransactionSynchronizationManager.getResource(this.connectionFactory) == null); try { - - boolean ackRequired = !this.acknowledgeMode.isAutoAck() && !this.acknowledgeMode.isManual(); + boolean ackRequired = forceAck || (!this.acknowledgeMode.isAutoAck() && !this.acknowledgeMode.isManual()); if (ackRequired && (!this.transactional || isLocallyTransacted)) { - long deliveryTag = new ArrayList(this.deliveryTags).get(this.deliveryTags.size() - 1); + long deliveryTag = new ArrayList<>(this.deliveryTags).get(this.deliveryTags.size() - 1); try { this.channel.basicAck(deliveryTag, true); notifyMessageAckListener(true, deliveryTag, null); @@ -909,14 +923,12 @@ public boolean commitIfNecessary(boolean localTx) { // For manual acks we still need to commit RabbitUtils.commitIfNecessary(this.channel); } - } finally { this.deliveryTags.clear(); } return true; - } /** @@ -931,7 +943,7 @@ private void notifyMessageAckListener(boolean success, long deliveryTag, @Nullab this.messageAckListener.onComplete(success, deliveryTag, cause); } catch (Exception e) { - logger.error("An exception occured in MessageAckListener.", e); + logger.error("An exception occurred in MessageAckListener.", e); } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java index 737ee14716..9a4ffb3705 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java @@ -41,6 +41,7 @@ import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessagePostProcessor; import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.batch.BatchingStrategy; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactoryUtils; import org.springframework.amqp.rabbit.connection.ConsumerChannelRegistry; @@ -134,6 +135,8 @@ public class SimpleMessageListenerContainer extends AbstractMessageListenerConta private long consumerStartTimeout = DEFAULT_CONSUMER_START_TIMEOUT; + private boolean enforceImmediateAckForManual; + private volatile int concurrentConsumers = 1; private volatile Integer maxConcurrentConsumers; @@ -504,6 +507,18 @@ public void setConsumerStartTimeout(long consumerStartTimeout) { this.consumerStartTimeout = consumerStartTimeout; } + /** + * Set to {@code true} to enforce {@link Channel#basicAck(long, boolean)} + * for {@link org.springframework.amqp.core.AcknowledgeMode#MANUAL} + * when {@link ImmediateAcknowledgeAmqpException} is thrown. + * This might be a tentative solution to not break behavior for current minor version. + * @param enforceImmediateAckForManual the flag to ack message for MANUAL mode on ImmediateAcknowledgeAmqpException + * @since 3.1.2 + */ + public void setEnforceImmediateAckForManual(boolean enforceImmediateAckForManual) { + this.enforceImmediateAckForManual = enforceImmediateAckForManual; + } + /** * Avoid the possibility of not configuring the CachingConnectionFactory in sync with the number of concurrent * consumers. @@ -1012,6 +1027,7 @@ private boolean doReceiveAndExecute(BlockingQueueConsumer consumer) throws Excep List messages = null; long deliveryTag = 0; + boolean immediateAck = false; boolean isBatchReceiveTimeoutEnabled = this.batchReceiveTimeout > 0; long startTime = isBatchReceiveTimeoutEnabled ? System.currentTimeMillis() : 0; for (int i = 0; i < this.batchSize; i++) { @@ -1050,9 +1066,9 @@ private boolean doReceiveAndExecute(BlockingQueueConsumer consumer) throws Excep if (messages == null) { messages = new ArrayList<>(this.batchSize); } - if (isDeBatchingEnabled() && getBatchingStrategy().canDebatch(message.getMessageProperties())) { - final List messageList = messages; - getBatchingStrategy().deBatch(message, fragment -> messageList.add(fragment)); + BatchingStrategy batchingStrategy = getBatchingStrategy(); + if (isDeBatchingEnabled() && batchingStrategy.canDebatch(message.getMessageProperties())) { + batchingStrategy.deBatch(message, messages::add); } else { messages.add(message); @@ -1073,6 +1089,7 @@ private boolean doReceiveAndExecute(BlockingQueueConsumer consumer) throws Excep + e.getMessage() + "': " + message.getMessageProperties().getDeliveryTag()); } + immediateAck = this.enforceImmediateAckForManual; break; } catch (Exception ex) { @@ -1081,6 +1098,7 @@ private boolean doReceiveAndExecute(BlockingQueueConsumer consumer) throws Excep this.logger.debug("User requested ack for failed delivery: " + message.getMessageProperties().getDeliveryTag()); } + immediateAck = this.enforceImmediateAckForManual; break; } long tagToRollback = isAsyncReplies() @@ -1117,14 +1135,14 @@ private boolean doReceiveAndExecute(BlockingQueueConsumer consumer) throws Excep } } if (messages != null) { - executeWithList(channel, messages, deliveryTag, consumer); + immediateAck = executeWithList(channel, messages, deliveryTag, consumer); } - return consumer.commitIfNecessary(isChannelLocallyTransacted()); + return consumer.commitIfNecessary(isChannelLocallyTransacted(), immediateAck); } - private void executeWithList(Channel channel, List messages, long deliveryTag, + private boolean executeWithList(Channel channel, List messages, long deliveryTag, BlockingQueueConsumer consumer) { try { @@ -1136,7 +1154,7 @@ private void executeWithList(Channel channel, List messages, long deliv + e.getMessage() + "' (last in batch): " + deliveryTag); } - return; + return this.enforceImmediateAckForManual; } catch (Exception ex) { if (causeChainHasImmediateAcknowledgeAmqpException(ex)) { @@ -1144,7 +1162,7 @@ private void executeWithList(Channel channel, List messages, long deliv this.logger.debug("User requested ack for failed delivery (last in batch): " + deliveryTag); } - return; + return this.enforceImmediateAckForManual; } if (getTransactionManager() != null) { if (getTransactionAttribute().rollbackOn(ex)) { @@ -1173,6 +1191,7 @@ private void executeWithList(Channel channel, List messages, long deliv throw ex; } } + return false; } protected void handleStartupFailure(BackOffExecution backOffExecution) { diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerManualAckIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerManualAckIntegrationTests.java index beb265e8d5..9eb3fe9902 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerManualAckIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerManualAckIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -17,6 +17,7 @@ package org.springframework.amqp.rabbit.listener; import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -27,8 +28,10 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.amqp.ImmediateAcknowledgeAmqpException; import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.core.Queue; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitTemplate; @@ -45,6 +48,7 @@ * @author Dave Syer * @author Gunnar Hillert * @author Gary Russell + * @author Artem Bilan * * @since 1.0 * @@ -56,7 +60,7 @@ public class MessageListenerManualAckIntegrationTests { public static final String TEST_QUEUE = "test.queue.MessageListenerManualAckIntegrationTests"; - private static Log logger = LogFactory.getLog(MessageListenerManualAckIntegrationTests.class); + private static final Log logger = LogFactory.getLog(MessageListenerManualAckIntegrationTests.class); private final Queue queue = new Queue(TEST_QUEUE); @@ -121,6 +125,26 @@ public void testListenerWithManualAckTransactional() throws Exception { assertThat(template.receiveAndConvert(queue.getName())).isNull(); } + @Test + public void immediateIsAckedForManual() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + container = createContainer(new ImmediateTestListener(latch)); + container.setEnforceImmediateAckForManual(true); + + template.convertAndSend(queue.getName(), "test data"); + + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + + container.stop(); + + Channel channel = template.getConnectionFactory().createConnection().createChannel(false); + + await().untilAsserted(() -> assertThat(channel.consumerCount(queue.getName())).isEqualTo(0)); + assertThat(channel.messageCount(queue.getName())).isEqualTo(0); + + channel.close(); + } + private SimpleMessageListenerContainer createContainer(Object listener) { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(template.getConnectionFactory()); container.setMessageListener(new MessageListenerAdapter(listener)); @@ -159,4 +183,23 @@ public void onMessage(Message message, Channel channel) throws Exception { } } + static class ImmediateTestListener implements MessageListener { + + private final CountDownLatch latch; + + ImmediateTestListener(CountDownLatch latch) { + this.latch = latch; + } + + @Override + public void onMessage(Message message) { + try { + throw new ImmediateAcknowledgeAmqpException("intentional"); + } + finally { + this.latch.countDown(); + } + } + } + } From e7e3e143cfb45b70571011600e70cebc827ed1db Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 2 Feb 2024 13:56:30 -0500 Subject: [PATCH 373/737] Fix Javadocs in the SimpleRabbitListenerContainerFactory --- .../config/SimpleRabbitListenerContainerFactory.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerContainerFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerContainerFactory.java index e0aebfffcc..c1739e540f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerContainerFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerContainerFactory.java @@ -16,9 +16,6 @@ package org.springframework.amqp.rabbit.config; -import com.rabbitmq.client.Channel; - -import org.springframework.amqp.ImmediateAcknowledgeAmqpException; import org.springframework.amqp.rabbit.listener.RabbitListenerEndpoint; import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; import org.springframework.amqp.utils.JavaUtils; @@ -159,9 +156,9 @@ public void setConsumerBatchEnabled(boolean consumerBatchEnabled) { } /** - * Set to {@code true} to enforce {@link Channel#basicAck(long, boolean)} + * Set to {@code true} to enforce {@link com.rabbitmq.client.Channel#basicAck(long, boolean)} * for {@link org.springframework.amqp.core.AcknowledgeMode#MANUAL} - * when {@link ImmediateAcknowledgeAmqpException} is thrown. + * when {@link org.springframework.amqp.ImmediateAcknowledgeAmqpException} is thrown. * This might be a tentative solution to not break behavior for current minor version. * @param enforceImmediateAckForManual the flag to ack message for MANUAL mode on ImmediateAcknowledgeAmqpException * @since 3.1.2 From 37d96412a1fc92d349f05976eb179cb7d8da0bd6 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 5 Feb 2024 12:49:44 -0500 Subject: [PATCH 374/737] SMLC: defer counter release until fetched messages are processed Currently, the `SimpleMessageListenerContainer` is stopped immediately when `cancelOK` is received. The expectation do not stop an application context until all the fetched messages are processed. Therefore, move `this.activeObjectCounter.release(this);` to the `BlockingQueueConsumer.nextMessage()` if the internal queue is empty and `cancelled` has been requested. * Adjust all the SMLC tests for a shorter `receiveTimeout` to not have blocking for nothing * Fix `EnableRabbitIntegrationTests.exec1` bean for `setAcceptTasksAfterContextClose(true)`. Looks like an `Executor` can be stopped by the application context before listener container is able to schedule its shutdown * Also add `System.setProperty("spring.amqp.deserialization.trust.all", "true");` to be able to run tests from IDE **Cherry-pick to `3.0.x`** --- .../examples/TestRabbitTemplateTests.java | 3 +- .../listener/BlockingQueueConsumer.java | 2 +- .../amqp/rabbit/AsyncRabbitTemplateTests.java | 16 +++++----- .../AbstractRabbitAnnotationDrivenTests.java | 20 +++++++------ .../EnableRabbitIntegrationTests.java | 17 +++++++---- .../annotation/MockMultiRabbitTests.java | 3 +- ...tenerAnnotationBeanPostProcessorTests.java | 4 ++- .../LocalizedQueueConnectionFactoryTests.java | 11 +++---- .../RoutingConnectionFactoryTests.java | 5 ++-- .../core/BatchingRabbitTemplateTests.java | 22 +++++++------- .../core/FixedReplyQueueDeadLetterTests.java | 5 +++- .../core/RabbitTemplateIntegrationTests.java | 15 ++++++---- .../RabbitTemplateMPPIntegrationTests.java | 3 +- ...atePublisherCallbacksIntegrationTests.java | 30 +++++++++---------- .../BrokerDeclaredQueueNameTests.java | 5 ++-- .../rabbit/listener/ContainerAdminTests.java | 3 +- .../ContainerInitializationTests.java | 3 +- .../listener/ContainerShutDownTests.java | 2 ++ .../JavaConfigFixedReplyQueueTests.java | 5 +++- .../listener/LocallyTransactedSMLCTests.java | 5 ++-- ...ContainerErrorHandlerIntegrationTests.java | 5 ++-- ...nerContainerLifecycleIntegrationTests.java | 23 ++++++++------ ...ontainerMultipleQueueIntegrationTests.java | 7 +++-- ...istenerContainerRetryIntegrationTests.java | 6 ++-- .../MessageListenerContainerTxSynchTests.java | 3 +- ...sageListenerManualAckIntegrationTests.java | 1 + ...MessageListenerTxSizeIntegrationTests.java | 6 ++-- .../listener/QueueDeclarationTests.java | 3 +- ...ageListenerContainerIntegration2Tests.java | 8 ++++- ...mpleMessageListenerContainerLongTests.java | 12 +++++--- .../SimpleMessageListenerContainerTests.java | 16 ++++++++-- .../SimpleMessageListenerWithRabbitMQ.java | 3 +- .../logback/AmqpAppenderConfiguration.java | 3 +- .../rabbit/retry/MissingIdRetryTests.java | 7 +++-- .../annotation/EnableRabbitKotlinTests.kt | 3 +- 35 files changed, 178 insertions(+), 107 deletions(-) diff --git a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/TestRabbitTemplateTests.java b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/TestRabbitTemplateTests.java index 8063132372..4d8c69586b 100644 --- a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/TestRabbitTemplateTests.java +++ b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/TestRabbitTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 the original author or authors. + * Copyright 2017-2024 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. @@ -135,6 +135,7 @@ public String baz(String in) { public SimpleMessageListenerContainer smlc1() throws IOException { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory()); container.setQueueNames("foo", "bar"); + container.setReceiveTimeout(10); container.setMessageListener(new MessageListenerAdapter(new Object() { @SuppressWarnings("unused") diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java index f78a3fe0e0..0d14eebaef 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java @@ -558,6 +558,7 @@ public Message nextMessage(long timeout) throws InterruptedException, ShutdownSi } Message message = handle(this.queue.poll(timeout, TimeUnit.MILLISECONDS)); if (message == null && this.cancelled.get()) { + this.activeObjectCounter.release(this); throw new ConsumerCancelledException(); } return message; @@ -1018,7 +1019,6 @@ public void handleCancelOk(String consumerTag) { + "); " + BlockingQueueConsumer.this); } this.canceled = true; - BlockingQueueConsumer.this.activeObjectCounter.release(BlockingQueueConsumer.this); } @Override diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java index b8a925f43d..eccba25c8f 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 the original author or authors. + * Copyright 2016-2024 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. @@ -383,17 +383,17 @@ public void testStopCancelled() throws Exception { @Test void ctorCoverage() { AsyncRabbitTemplate template = new AsyncRabbitTemplate(mock(ConnectionFactory.class), "ex", "rk"); - assertThat(template).extracting(t -> t.getRabbitTemplate()) + assertThat(template).extracting(AsyncRabbitTemplate::getRabbitTemplate) .extracting("exchange") .isEqualTo("ex"); - assertThat(template).extracting(t -> t.getRabbitTemplate()) + assertThat(template).extracting(AsyncRabbitTemplate::getRabbitTemplate) .extracting("routingKey") .isEqualTo("rk"); template = new AsyncRabbitTemplate(mock(ConnectionFactory.class), "ex", "rk", "rq"); - assertThat(template).extracting(t -> t.getRabbitTemplate()) + assertThat(template).extracting(AsyncRabbitTemplate::getRabbitTemplate) .extracting("exchange") .isEqualTo("ex"); - assertThat(template).extracting(t -> t.getRabbitTemplate()) + assertThat(template).extracting(AsyncRabbitTemplate::getRabbitTemplate) .extracting("routingKey") .isEqualTo("rk"); assertThat(template) @@ -403,10 +403,10 @@ void ctorCoverage() { .extracting("queueNames") .isEqualTo(new String[] { "rq" }); template = new AsyncRabbitTemplate(mock(ConnectionFactory.class), "ex", "rk", "rq", "ra"); - assertThat(template).extracting(t -> t.getRabbitTemplate()) + assertThat(template).extracting(AsyncRabbitTemplate::getRabbitTemplate) .extracting("exchange") .isEqualTo("ex"); - assertThat(template).extracting(t -> t.getRabbitTemplate()) + assertThat(template).extracting(AsyncRabbitTemplate::getRabbitTemplate) .extracting("routingKey") .isEqualTo("rk"); assertThat(template) @@ -523,6 +523,7 @@ public RabbitTemplate templateForDirect(ConnectionFactory connectionFactory) { @Primary public SimpleMessageListenerContainer replyContainer(ConnectionFactory connectionFactory) { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); + container.setReceiveTimeout(10); container.setAfterReceivePostProcessors(new GUnzipPostProcessor()); container.setQueueNames(replies().getName()); return container; @@ -545,6 +546,7 @@ public AsyncRabbitTemplate asyncDirectTemplate( @Bean public SimpleMessageListenerContainer remoteContainer(ConnectionFactory connectionFactory) { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); + container.setReceiveTimeout(10); container.setQueueNames(requests().getName()); container.setAfterReceivePostProcessors(new GUnzipPostProcessor()); MessageListenerAdapter messageListener = diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AbstractRabbitAnnotationDrivenTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AbstractRabbitAnnotationDrivenTests.java index 14bbb11813..271731d2af 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AbstractRabbitAnnotationDrivenTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AbstractRabbitAnnotationDrivenTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * Copyright 2014-2024 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. @@ -142,6 +142,7 @@ public void testFullConfiguration(ApplicationContext context) { // Resolve the container and invoke a message on it SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); + container.setReceiveTimeout(10); endpoint.setupListenerContainer(container); MessagingMessageListenerAdapter listener = (MessagingMessageListenerAdapter) container.getMessageListener(); @@ -158,9 +159,9 @@ public void testFullConfiguration(ApplicationContext context) { } /** - * Test for {@link CustomBean} and an manually endpoint registered + * Test for {@link CustomBean} and a manually registered endpoint * with "myCustomEndpointId". The custom endpoint does not provide - * any factory so it's registered with the default one + * any factory, so it's registered with the default one */ public void testCustomConfiguration(ApplicationContext context) { RabbitListenerContainerTestFactory defaultFactory = @@ -171,14 +172,15 @@ public void testCustomConfiguration(ApplicationContext context) { assertThat(customFactory.getListenerContainers()).hasSize(1); RabbitListenerEndpoint endpoint = defaultFactory.getListenerContainers().get(0).getEndpoint(); assertThat(endpoint.getClass()).as("Wrong endpoint type").isEqualTo(SimpleRabbitListenerEndpoint.class); - assertThat(((SimpleRabbitListenerEndpoint) endpoint).getMessageListener()).as("Wrong listener set in custom endpoint").isEqualTo(context.getBean("simpleMessageListener")); + assertThat(((SimpleRabbitListenerEndpoint) endpoint).getMessageListener()) + .as("Wrong listener set in custom endpoint").isEqualTo(context.getBean("simpleMessageListener")); RabbitListenerEndpointRegistry customRegistry = context.getBean("customRegistry", RabbitListenerEndpointRegistry.class); - assertThat(customRegistry.getListenerContainerIds().size()).as("Wrong number of containers in the registry").isEqualTo(2); - assertThat(customRegistry.getListenerContainers().size()).as("Wrong number of containers in the registry").isEqualTo(2); - assertThat(customRegistry.getListenerContainer("listenerId")).as("Container with custom id on the annotation should be found").isNotNull(); - assertThat(customRegistry.getListenerContainer("myCustomEndpointId")).as("Container created with custom id should be found").isNotNull(); + assertThat(customRegistry.getListenerContainerIds()).hasSize(2); + assertThat(customRegistry.getListenerContainers()).hasSize(2); + assertThat(customRegistry.getListenerContainer("listenerId")).isNotNull(); + assertThat(customRegistry.getListenerContainer("myCustomEndpointId")).isNotNull(); } /** @@ -205,7 +207,6 @@ public void testDefaultContainerFactoryConfiguration(ApplicationContext context) /** * Test for {@link ValidationBean} with a validator ({@link TestValidator}) specified * in a custom {@link org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory}. - * * The test should throw a {@link org.springframework.amqp.rabbit.support.ListenerExecutionFailedException} */ public void testRabbitHandlerMethodFactoryConfiguration(ApplicationContext context) throws Exception { @@ -216,6 +217,7 @@ public void testRabbitHandlerMethodFactoryConfiguration(ApplicationContext conte simpleFactory.getListenerContainers().get(0).getEndpoint(); SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); + container.setReceiveTimeout(10); endpoint.setupListenerContainer(container); MessagingMessageListenerAdapter listener = (MessagingMessageListenerAdapter) container.getMessageListener(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java index 41a794f036..856dc04825 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2024 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. @@ -41,6 +41,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -249,6 +250,7 @@ public class EnableRabbitIntegrationTests extends NeedsManagementTests { public static void setUp() { System.setProperty(RabbitListenerAnnotationBeanPostProcessor.RABBIT_EMPTY_STRING_ARGUMENTS_PROPERTY, "test-empty"); + System.setProperty("spring.amqp.deserialization.trust.all", "true"); RabbitAvailableCondition.getBrokerRunning().removeExchanges("auto.exch.tx", "auto.exch", "auto.exch.fanout", @@ -827,7 +829,7 @@ public void testMeta() throws Exception { } @Test - public void testHeadersExchange() throws Exception { + public void testHeadersExchange() { assertThat(rabbitTemplate.convertSendAndReceive("auto.headers", "", "foo", message -> { message.getMessageProperties().getHeaders().put("foo", "bar"); @@ -846,7 +848,7 @@ public void deadLetterOnDefaultExchange() { this.rabbitTemplate.convertAndSend("amqp656", "foo"); assertThat(this.rabbitTemplate.receiveAndConvert("amqp656dlq", 10000)).isEqualTo("foo"); try { - Map amqp656 = await().until(() -> queueInfo("amqp656"), q -> q != null); + Map amqp656 = await().until(() -> queueInfo("amqp656"), Objects::nonNull); if (amqp656 != null) { assertThat(arguments(amqp656).get("test-empty")).isEqualTo(""); assertThat(arguments(amqp656).get("test-null")).isEqualTo("undefined"); @@ -961,7 +963,7 @@ public void messagingMessageReturned() throws InterruptedException { catch (@SuppressWarnings("unused") Exception e) { return null; } - }, tim -> tim != null); + }, Objects::nonNull); assertThat(timer.count()).isEqualTo(1L); } @@ -1790,6 +1792,7 @@ public SimpleRabbitListenerContainerFactory consumerBatchContainerFactory() { factory.setBatchListener(true); factory.setBatchSize(2); factory.setConsumerBatchEnabled(true); + factory.setReceiveTimeout(10L); return factory; } @@ -1865,7 +1868,7 @@ public CountDownLatch errorHandlerLatch2() { @Bean public AtomicReference errorHandlerError() { - return new AtomicReference(); + return new AtomicReference<>(); } @Bean @@ -1947,7 +1950,9 @@ public TxService txService() { @Bean public TaskExecutor exec1() { - return new ThreadPoolTaskExecutor(); + ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); + threadPoolTaskExecutor.setAcceptTasksAfterContextClose(true); + return threadPoolTaskExecutor; } // Rabbit infrastructure setup diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MockMultiRabbitTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MockMultiRabbitTests.java index 3aa30d5845..cda0362fe2 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MockMultiRabbitTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MockMultiRabbitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-2024 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. @@ -83,6 +83,7 @@ void multipleSimpleMessageListeners() { Assertions.assertThat(methodEndpoint.getMethod()).isNotNull(); SimpleMessageListenerContainer listenerContainer = new SimpleMessageListenerContainer(); + listenerContainer.setReceiveTimeout(10); methodEndpoint.setupListenerContainer(listenerContainer); Assertions.assertThat(listenerContainer.getMessageListener()).isNotNull(); }); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessorTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessorTests.java index 29b6518906..6ef00c5932 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessorTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -86,6 +86,7 @@ public void simpleMessageListener() { assertThat(methodEndpoint.getMethod()).isNotNull(); SimpleMessageListenerContainer listenerContainer = new SimpleMessageListenerContainer(); + listenerContainer.setReceiveTimeout(10); methodEndpoint.setupListenerContainer(listenerContainer); assertThat(listenerContainer.getMessageListener()).isNotNull(); @@ -114,6 +115,7 @@ public void simpleMessageListenerWithMixedAnnotations() { assertThat(iterator.next()).isEqualTo("secondQueue"); SimpleMessageListenerContainer listenerContainer = new SimpleMessageListenerContainer(); + listenerContainer.setReceiveTimeout(10); methodEndpoint.setupListenerContainer(listenerContainer); assertThat(listenerContainer.getMessageListener()).isNotNull(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryTests.java index 9dc9246b20..29192e6a62 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2022 the original author or authors. + * Copyright 2015-2024 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. @@ -63,11 +63,11 @@ */ public class LocalizedQueueConnectionFactoryTests { - private final Map channels = new HashMap(); + private final Map channels = new HashMap<>(); - private final Map consumers = new HashMap(); + private final Map consumers = new HashMap<>(); - private final Map consumerTags = new HashMap(); + private final Map consumerTags = new HashMap<>(); @Test public void testFailOver() throws Exception { @@ -83,7 +83,7 @@ public void testFailOver() throws Exception { final AtomicBoolean firstServer = new AtomicBoolean(true); final WebClient client1 = doCreateClient(adminUris[0], username, password, nodes[0]); final WebClient client2 = doCreateClient(adminUris[1], username, password, nodes[1]); - final Map mockCFs = new HashMap(); + final Map mockCFs = new HashMap<>(); CountDownLatch latch1 = new CountDownLatch(1); CountDownLatch latch2 = new CountDownLatch(1); mockCFs.put(rabbit1, mockCF(rabbit1, latch1)); @@ -116,6 +116,7 @@ public WebClient createClient(String username, String password) { willAnswer(new CallsRealMethods()).given(logger).debug(anyString()); ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(lqcf); + container.setReceiveTimeout(10); container.setQueueNames("q"); container.afterPropertiesSet(); container.start(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/RoutingConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/RoutingConnectionFactoryTests.java index ff5918bcec..6c6b7f5f32 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/RoutingConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/RoutingConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -170,7 +170,7 @@ protected Object determineCurrentLookupKey() { public void testAbstractRoutingConnectionFactoryWithListenerContainer() { ConnectionFactory connectionFactory1 = mock(ConnectionFactory.class); ConnectionFactory connectionFactory2 = mock(ConnectionFactory.class); - Map factories = new HashMap(2); + Map factories = new HashMap<>(2); factories.put("[baz]", connectionFactory1); factories.put("[foo,bar]", connectionFactory2); ConnectionFactory defaultConnectionFactory = mock(ConnectionFactory.class); @@ -181,6 +181,7 @@ public void testAbstractRoutingConnectionFactoryWithListenerContainer() { connectionFactory.setTargetConnectionFactories(factories); SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); + container.setReceiveTimeout(10); container.setQueueNames("foo", "bar"); container.afterPropertiesSet(); container.start(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplateTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplateTests.java index e623ed0460..e5587a979f 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplateTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2024 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. @@ -236,14 +236,14 @@ public void testSimpleBatchTwoEqualBufferLimit() throws Exception { @Test void testDebatchSMLCSplit() throws Exception { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(this.connectionFactory); - container.setReceiveTimeout(100); + container.setReceiveTimeout(10); testDebatchByContainer(container, false); } @Test void testDebatchSMLC() throws Exception { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(this.connectionFactory); - container.setReceiveTimeout(100); + container.setReceiveTimeout(10); testDebatchByContainer(container, true); } @@ -303,16 +303,16 @@ private void testDebatchByContainer(AbstractMessageListenerContainer container, @Test public void testDebatchByContainerPerformance() throws Exception { - final List received = new ArrayList(); + final List received = new ArrayList<>(); int count = 100000; final CountDownLatch latch = new CountDownLatch(count); SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(this.connectionFactory); container.setQueueNames(ROUTE); - container.setMessageListener((MessageListener) message -> { + container.setMessageListener(message -> { received.add(message); latch.countDown(); }); - container.setReceiveTimeout(100); + container.setReceiveTimeout(10); container.setPrefetchCount(1000); container.setBatchSize(1000); container.afterPropertiesSet(); @@ -344,8 +344,8 @@ public void testDebatchByContainerPerformance() throws Exception { public void testDebatchByContainerBadMessageRejected() throws Exception { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(this.connectionFactory); container.setQueueNames(ROUTE); - container.setMessageListener((MessageListener) message -> { }); - container.setReceiveTimeout(100); + container.setMessageListener(message -> { }); + container.setReceiveTimeout(10); ConditionalRejectingErrorHandler errorHandler = new ConditionalRejectingErrorHandler(); container.setErrorHandler(errorHandler); container.afterPropertiesSet(); @@ -632,15 +632,15 @@ private Message receive(BatchingRabbitTemplate template) throws InterruptedExcep @Test public void testCompressionWithContainer() throws Exception { - final List received = new ArrayList(); + final List received = new ArrayList<>(); final CountDownLatch latch = new CountDownLatch(2); SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(this.connectionFactory); container.setQueueNames(ROUTE); - container.setMessageListener((MessageListener) message -> { + container.setMessageListener(message -> { received.add(message); latch.countDown(); }); - container.setReceiveTimeout(100); + container.setReceiveTimeout(10); container.setAfterReceivePostProcessors(new DelegatingDecompressingPostProcessor()); container.afterPropertiesSet(); container.start(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/FixedReplyQueueDeadLetterTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/FixedReplyQueueDeadLetterTests.java index b132f42d1b..f4145c41d3 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/FixedReplyQueueDeadLetterTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/FixedReplyQueueDeadLetterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2024 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. @@ -176,6 +176,7 @@ public SimpleMessageListenerContainer replyListenerContainer() { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); container.setConnectionFactory(rabbitConnectionFactory()); container.setQueues(replyQueue()); + container.setReceiveTimeout(10); container.setMessageListener(fixedReplyQRabbitTemplate()); return container; } @@ -188,6 +189,7 @@ public SimpleMessageListenerContainer serviceListenerContainer() { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); container.setConnectionFactory(rabbitConnectionFactory()); container.setQueues(requestQueue()); + container.setReceiveTimeout(10); container.setMessageListener(new MessageListenerAdapter(new PojoListener())); return container; } @@ -200,6 +202,7 @@ public SimpleMessageListenerContainer dlListenerContainer() { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); container.setConnectionFactory(rabbitConnectionFactory()); container.setQueues(dlq()); + container.setReceiveTimeout(10); container.setMessageListener(new MessageListenerAdapter(deadListener())); return container; } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateIntegrationTests.java index 647e931b26..00dea012c1 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -722,6 +722,7 @@ public void testAtomicSendAndReceiveUserCorrelation() throws Exception { template.setReplyTimeout(10000); SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(cachingConnectionFactory); container.setQueues(replyQueue); + container.setReceiveTimeout(10); container.setMessageListener(template); container.afterPropertiesSet(); container.start(); @@ -729,10 +730,10 @@ public void testAtomicSendAndReceiveUserCorrelation() throws Exception { messageProperties.setCorrelationId("myCorrelationId"); Message message = new Message("test-message".getBytes(), messageProperties); Message reply = template.sendAndReceive(message); - assertThat(new String(received.get(1000, TimeUnit.MILLISECONDS).getBody())).isEqualTo(new String(message.getBody())); + assertThat(received.get(1000, TimeUnit.MILLISECONDS).getBody()).isEqualTo(message.getBody()); assertThat(reply).as("Reply is expected").isNotNull(); assertThat(remoteCorrelationId.get()).isEqualTo("myCorrelationId"); - assertThat(new String(reply.getBody())).isEqualTo(new String(message.getBody())); + assertThat(reply.getBody()).isEqualTo(message.getBody()); // Message was consumed so nothing left on queue reply = template.receive(); assertThat(reply).isEqualTo(null); @@ -1236,12 +1237,13 @@ public void testSymmetricalReceiveAndReply() throws InterruptedException { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); container.setConnectionFactory(template.getConnectionFactory()); container.setQueues(REPLY_QUEUE); + container.setReceiveTimeout(10); container.setMessageListener(template); container.start(); int count = 10; - final Map results = new ConcurrentHashMap(); + final Map results = new ConcurrentHashMap<>(); ExecutorService executor = Executors.newFixedThreadPool(10); @@ -1344,7 +1346,8 @@ private void sendAndReceiveFastGuts(boolean tempQueue, boolean setDirectReplyToE SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); container.setConnectionFactory(template.getConnectionFactory()); container.setQueueNames(ROUTE); - final AtomicReference replyToWas = new AtomicReference(); + container.setReceiveTimeout(10); + final AtomicReference replyToWas = new AtomicReference<>(); MessageListenerAdapter messageListenerAdapter = new MessageListenerAdapter(new Object() { @SuppressWarnings("unused") @@ -1394,7 +1397,7 @@ public String handleMessage(String message) { }); messageListener.setBeforeSendReplyPostProcessors(new GZipPostProcessor()); container.setMessageListener(messageListener); - container.setReceiveTimeout(100); + container.setReceiveTimeout(10); container.afterPropertiesSet(); container.start(); RabbitTemplate template = createSendAndReceiveRabbitTemplate(this.template.getConnectionFactory()); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateMPPIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateMPPIntegrationTests.java index dd5a85b02f..97f37a3e8b 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateMPPIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateMPPIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 the original author or authors. + * Copyright 2017-2024 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. @@ -93,6 +93,7 @@ public void testMPPsAppliedReplyContainerTests() { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(this.config.cf()); try { container.setQueueNames(REPLIES); + container.setReceiveTimeout(10); container.setMessageListener(this.template); container.afterPropertiesSet(); container.start(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests.java index e8c4150215..5d68698991 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -29,6 +29,7 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.ConcurrentModificationException; @@ -219,13 +220,14 @@ public Message postProcessMessage(Message message, Correlation correlation, Stri @Test public void testPublisherConfirmWithSendAndReceive() throws Exception { final CountDownLatch latch = new CountDownLatch(1); - final AtomicReference confirmCD = new AtomicReference(); + final AtomicReference confirmCD = new AtomicReference<>(); templateWithConfirmsEnabled.setConfirmCallback((correlationData, ack, cause) -> { confirmCD.set(correlationData); latch.countDown(); }); SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(this.connectionFactoryWithConfirmsEnabled); container.setQueueNames(ROUTE); + container.setReceiveTimeout(10); container.setMessageListener( new MessageListenerAdapter((ReplyingMessageListener) String::toUpperCase)); container.start(); @@ -285,7 +287,7 @@ public void testPublisherConfirmReceivedTwoTemplates() throws Exception { @Test public void testPublisherReturns() throws Exception { final CountDownLatch latch = new CountDownLatch(1); - final List returns = new ArrayList(); + final List returns = new ArrayList<>(); templateWithReturnsEnabled.setReturnsCallback((returned) -> { returns.add(returned.getMessage()); latch.countDown(); @@ -295,13 +297,13 @@ public void testPublisherReturns() throws Exception { assertThat(latch.await(1000, TimeUnit.MILLISECONDS)).isTrue(); assertThat(returns).hasSize(1); Message message = returns.get(0); - assertThat(new String(message.getBody(), "utf-8")).isEqualTo("message"); + assertThat(new String(message.getBody(), StandardCharsets.UTF_8)).isEqualTo("message"); } @Test public void testPublisherReturnsWithMandatoryExpression() throws Exception { final CountDownLatch latch = new CountDownLatch(1); - final List returns = new ArrayList(); + final List returns = new ArrayList<>(); templateWithReturnsEnabled.setReturnsCallback((returned) -> { returns.add(returned.getMessage()); latch.countDown(); @@ -313,7 +315,7 @@ public void testPublisherReturnsWithMandatoryExpression() throws Exception { assertThat(latch.await(1000, TimeUnit.MILLISECONDS)).isTrue(); assertThat(returns).hasSize(1); Message message = returns.get(0); - assertThat(new String(message.getBody(), "utf-8")).isEqualTo("message"); + assertThat(new String(message.getBody(), StandardCharsets.UTF_8)).isEqualTo("message"); } @Test @@ -416,7 +418,7 @@ public void testPublisherConfirmNotReceivedMultiThreads() throws Exception { exec.shutdown(); assertThat(exec.awaitTermination(10, TimeUnit.SECONDS)).isTrue(); ccf.destroy(); - await().until(() -> pendingConfirms.size() == 0); + await().until(pendingConfirms::isEmpty); } @Test @@ -500,7 +502,6 @@ public void testPublisherConfirmMultiple() throws Exception { /** * Tests that piggy-backed confirms (multiple=true) are distributed to the proper * template. - * @throws Exception */ @Test public void testPublisherConfirmMultipleWithTwoListeners() throws Exception { @@ -523,7 +524,7 @@ public void testPublisherConfirmMultipleWithTwoListeners() throws Exception { ccf.setPublisherConfirmType(ConfirmType.CORRELATED); final RabbitTemplate template1 = new RabbitTemplate(ccf); - final Set confirms = new HashSet(); + final Set confirms = new HashSet<>(); final CountDownLatch latch1 = new CountDownLatch(1); template1.setConfirmCallback((correlationData, ack, cause) -> { if (ack) { @@ -638,8 +639,8 @@ public void testConcurrentConfirms() throws Exception { @Test public void testNackForBadExchange() throws Exception { final AtomicBoolean nack = new AtomicBoolean(true); - final AtomicReference correlation = new AtomicReference(); - final AtomicReference reason = new AtomicReference(); + final AtomicReference correlation = new AtomicReference<>(); + final AtomicReference reason = new AtomicReference<>(); final CountDownLatch latch = new CountDownLatch(2); this.templateWithConfirmsEnabled.setConfirmCallback((correlationData, ack, cause) -> { nack.set(ack); @@ -648,7 +649,7 @@ public void testNackForBadExchange() throws Exception { latch.countDown(); }); Log logger = spy(TestUtils.getPropertyValue(connectionFactoryWithConfirmsEnabled, "logger", Log.class)); - final AtomicReference log = new AtomicReference(); + final AtomicReference log = new AtomicReference<>(); willAnswer(invocation -> { log.set((String) invocation.getArguments()[0]); invocation.callRealMethod(); @@ -735,7 +736,7 @@ public void testPublisherConfirmCloseConcurrencyDetectInAllPlaces() throws Excep * where the close can be detected. Run the test to verify these (and any future calls * that are added) properly emit the nacks. * - * The following will detect proper operation if any more calls are added in future. + * The following will detect proper operation if any more calls are added in the future. */ for (int i = 100; i < 110; i++) { testPublisherConfirmCloseConcurrency(i); @@ -792,7 +793,6 @@ private void testPublisherConfirmCloseConcurrency(final int closeAfter) throws E exec.shutdownNow(); } - @SuppressWarnings("unchecked") @Test public void testPublisherCallbackChannelImplCloseWithPending() throws Exception { @@ -825,7 +825,7 @@ public void testPublisherCallbackChannelImplCloseWithPending() throws Exception assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); Map pending = TestUtils.getPropertyValue(channel, "pendingConfirms", Map.class); - await().until(() -> pending.size() == 0); + await().until(pending::isEmpty); } @Test diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BrokerDeclaredQueueNameTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BrokerDeclaredQueueNameTests.java index d65698a333..434621ee2f 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BrokerDeclaredQueueNameTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BrokerDeclaredQueueNameTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * Copyright 2018-2024 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. @@ -171,7 +171,7 @@ public RabbitAdmin admin() { @Bean public AbstractMessageListenerContainer container() { - AbstractMessageListenerContainer container = new SimpleMessageListenerContainer(cf()); + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(cf()); container.setQueues(queue1()); container.setMessageListener(m -> { message().set(m); @@ -181,6 +181,7 @@ public AbstractMessageListenerContainer container() { container.setFailedDeclarationRetryInterval(100); container.setMissingQueuesFatal(false); container.setRecoveryInterval(100); + container.setReceiveTimeout(10); container.setAutoStartup(false); return container; } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerAdminTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerAdminTests.java index 62f478873f..22ef6ae445 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerAdminTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerAdminTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2024 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. @@ -45,6 +45,7 @@ void findAdminInParentContext() { parent.refresh(); GenericApplicationContext child = new GenericApplicationContext(parent); SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(cf); + container.setReceiveTimeout(10); child.registerBean(SimpleMessageListenerContainer.class, () -> container); child.refresh(); container.start(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerInitializationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerInitializationTests.java index 88d03d1f33..c0b87bed74 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerInitializationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerInitializationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 the original author or authors. + * Copyright 2016-2024 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. @@ -144,6 +144,7 @@ public ConnectionFactory connectionFactory() { public SimpleMessageListenerContainer container() { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory()); container.setQueueNames(TEST_MISMATCH); + container.setReceiveTimeout(10); container.setMessageListener(new MessageListenerAdapter(new Object() { @SuppressWarnings("unused") diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerShutDownTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerShutDownTests.java index db23cde289..1902921ad9 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerShutDownTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerShutDownTests.java @@ -46,6 +46,7 @@ public class ContainerShutDownTests { @Test public void testUninterruptibleListenerSMLC() throws Exception { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); + container.setReceiveTimeout(10); testUninterruptibleListener(container); } @@ -101,6 +102,7 @@ public void testUninterruptibleListener(AbstractMessageListenerContainer contain @Test public void consumersCorrectlyCancelledOnShutdownSMLC() throws Exception { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); + container.setReceiveTimeout(10); consumersCorrectlyCancelledOnShutdown(container); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/JavaConfigFixedReplyQueueTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/JavaConfigFixedReplyQueueTests.java index 7f5056fee5..d795411f46 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/JavaConfigFixedReplyQueueTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/JavaConfigFixedReplyQueueTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2024 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. @@ -170,6 +170,7 @@ public SimpleMessageListenerContainer replyListenerContainer() { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); container.setConnectionFactory(rabbitConnectionFactory()); container.setQueues(replyQueue()); + container.setReceiveTimeout(10); container.setMessageListener(fixedReplyQRabbitTemplate()); return container; } @@ -182,6 +183,7 @@ public SimpleMessageListenerContainer serviceListenerContainer() { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); container.setConnectionFactory(rabbitConnectionFactory()); container.setQueues(requestQueue()); + container.setReceiveTimeout(10); container.setMessageListener(new MessageListenerAdapter(new PojoListener())); return container; } @@ -194,6 +196,7 @@ public SimpleMessageListenerContainer replyListenerContainerWrongQueue() { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); container.setConnectionFactory(rabbitConnectionFactory()); container.setQueues(replyQueue()); + container.setReceiveTimeout(10); container.setMessageListener(fixedReplyQRabbitTemplateWrongQueue()); container.setAutoStartup(false); return container; diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/LocallyTransactedSMLCTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/LocallyTransactedSMLCTests.java index 043679f74b..60ebe309c7 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/LocallyTransactedSMLCTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/LocallyTransactedSMLCTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2024 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. @@ -27,7 +27,8 @@ public class LocallyTransactedSMLCTests extends LocallyTransactedTests { @Override protected AbstractMessageListenerContainer createContainer(AbstractConnectionFactory connectionFactory) { - AbstractMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); + container.setReceiveTimeout(10); return container; } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerErrorHandlerIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerErrorHandlerIntegrationTests.java index 627c8a7670..cba98cee3f 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerErrorHandlerIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerErrorHandlerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -101,6 +101,7 @@ public void testErrorHandlerThrowsARADRE() throws Exception { RabbitTemplate template = this.createTemplate(1); SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(template.getConnectionFactory()); container.setQueues(QUEUE); + container.setReceiveTimeout(10); final CountDownLatch messageReceived = new CountDownLatch(1); final CountDownLatch spiedQLogger = new CountDownLatch(1); final CountDownLatch errorHandled = new CountDownLatch(1); @@ -108,7 +109,7 @@ public void testErrorHandlerThrowsARADRE() throws Exception { errorHandled.countDown(); throw new AmqpRejectAndDontRequeueException("foo", t); }); - container.setMessageListener((MessageListener) message -> { + container.setMessageListener(message -> { try { messageReceived.countDown(); spiedQLogger.await(10, TimeUnit.SECONDS); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerLifecycleIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerLifecycleIntegrationTests.java index 6aeb1ea4b1..cb53e80b23 100755 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerLifecycleIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerLifecycleIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -38,7 +38,6 @@ import org.springframework.amqp.AmqpIllegalStateException; import org.springframework.amqp.core.AcknowledgeMode; -import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.core.Queue; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; @@ -71,9 +70,9 @@ */ @RabbitAvailable(queues = MessageListenerContainerLifecycleIntegrationTests.TEST_QUEUE) @LongRunning -@LogLevels(classes = { RabbitTemplate.class, - SimpleMessageListenerContainer.class, BlockingQueueConsumer.class, - MessageListenerContainerLifecycleIntegrationTests.class }, level = "INFO") +@LogLevels(classes = {RabbitTemplate.class, + SimpleMessageListenerContainer.class, BlockingQueueConsumer.class, + MessageListenerContainerLifecycleIntegrationTests.class}, level = "INFO") public class MessageListenerContainerLifecycleIntegrationTests { public static final String TEST_QUEUE = "test.queue.MessageListenerContainerLifecycleIntegrationTests"; @@ -84,6 +83,7 @@ public class MessageListenerContainerLifecycleIntegrationTests { private enum TransactionMode { ON, OFF, PREFETCH, PREFETCH_NO_TX; + public boolean isTransactional() { return this != OFF && this != PREFETCH_NO_TX; } @@ -103,6 +103,7 @@ public int getTxSize() { private enum Concurrency { LOW(1), HIGH(5); + private final int value; Concurrency(int value) { @@ -116,6 +117,7 @@ public int value() { private enum MessageCount { LOW(1), MEDIUM(20), HIGH(500); + private final int value; MessageCount(int value) { @@ -199,7 +201,7 @@ public void testBadCredentials() throws Exception { cf.setUsername("foo"); final CachingConnectionFactory connectionFactory = new CachingConnectionFactory(cf); assertThatExceptionOfType(AmqpIllegalStateException.class).isThrownBy(() -> - doTest(MessageCount.LOW, Concurrency.LOW, TransactionMode.OFF, template, connectionFactory)) + doTest(MessageCount.LOW, Concurrency.LOW, TransactionMode.OFF, template, connectionFactory)) .withCauseExactlyInstanceOf(FatalListenerStartupException.class); ((DisposableBean) template.getConnectionFactory()).destroy(); } @@ -334,7 +336,7 @@ public void testShutDownWithPrefetch() throws Exception { final AtomicInteger received = new AtomicInteger(); final CountDownLatch awaitConsumeFirst = new CountDownLatch(5); final CountDownLatch awaitConsumeSecond = new CountDownLatch(10); - container.setMessageListener((MessageListener) message -> { + container.setMessageListener(message -> { try { awaitStart1.countDown(); prefetched.await(10, TimeUnit.SECONDS); @@ -353,6 +355,7 @@ public void testShutDownWithPrefetch() throws Exception { container.setPrefetchCount(5); container.setQueueNames(queue.getName()); + container.setReceiveTimeout(10); container.afterPropertiesSet(); container.start(); @@ -364,7 +367,7 @@ public void testShutDownWithPrefetch() throws Exception { .getPropertyValue(container, "consumers"); await().until(() -> { if (consumers.size() > 0 - && TestUtils.getPropertyValue(consumers.iterator().next(), "queue", BlockingQueue.class).size() > 3) { + && TestUtils.getPropertyValue(consumers.iterator().next(), "queue", BlockingQueue.class).size() > 3) { prefetched.countDown(); return true; } @@ -411,6 +414,7 @@ public void testSimpleMessageListenerContainerStoppedWithoutWarn() throws Except DirectFieldAccessor dfa = new DirectFieldAccessor(container); dfa.setPropertyValue("logger", log); container.setQueues(queue); + container.setReceiveTimeout(10); container.setMessageListener(new MessageListenerAdapter()); container.afterPropertiesSet(); container.start(); @@ -484,7 +488,8 @@ public ConnectionFactory connectionFactory() { public SimpleMessageListenerContainer container() { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory()); container.setQueues(queue); - container.setMessageListener((MessageListener) message -> { + container.setReceiveTimeout(10); + container.setMessageListener(message -> { try { consumerLatch().countDown(); Thread.sleep(500); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerMultipleQueueIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerMultipleQueueIntegrationTests.java index 343fc7ff70..155eeaa885 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerMultipleQueueIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerMultipleQueueIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -90,8 +90,8 @@ private void doTest(int concurrentConsumers, ContainerConfigurer configurer) { messageConverter.setCreateMessageIds(true); template.setMessageConverter(messageConverter); for (int i = 0; i < messageCount; i++) { - template.convertAndSend(queue1.getName(), Integer.valueOf(i)); - template.convertAndSend(queue2.getName(), Integer.valueOf(i)); + template.convertAndSend(queue1.getName(), i); + template.convertAndSend(queue2.getName(), i); } final SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); final CountDownLatch latch = new CountDownLatch(messageCount * 2); @@ -100,6 +100,7 @@ private void doTest(int concurrentConsumers, ContainerConfigurer configurer) { container.setAcknowledgeMode(AcknowledgeMode.AUTO); container.setChannelTransacted(true); container.setConcurrentConsumers(concurrentConsumers); + container.setReceiveTimeout(10); configurer.configure(container); container.afterPropertiesSet(); container.start(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerRetryIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerRetryIntegrationTests.java index 5663575341..576ac75a8e 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerRetryIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerRetryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -116,7 +116,7 @@ private void doTestRetryWithBatchListener(boolean stateful) throws Exception { container.setBatchSize(2); final CountDownLatch latch = new CountDownLatch(1); - container.setAdviceChain(new Advice[] { createRetryInterceptor(latch, stateful, true) }); + container.setAdviceChain(createRetryInterceptor(latch, stateful, true)); container.setQueueNames(queue.getName()); container.setReceiveTimeout(50); @@ -252,7 +252,7 @@ private void doTestRetry(int messageCount, int txSize, int failFrequency, int co container.setConcurrentConsumers(concurrentConsumers); final CountDownLatch latch = new CountDownLatch(failedMessageCount); - container.setAdviceChain(new Advice[] { createRetryInterceptor(latch, stateful) }); + container.setAdviceChain(createRetryInterceptor(latch, stateful)); container.setQueueNames(queue.getName()); container.setReceiveTimeout(50); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerTxSynchTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerTxSynchTests.java index fb097e447c..cc0b9f8c72 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerTxSynchTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerTxSynchTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2024 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. @@ -88,6 +88,7 @@ void resourcesClearedAfterTxFails() throws IOException, TimeoutException, Interr mlc.setQueueNames("foo"); mlc.setTaskExecutor(exec); mlc.setChannelTransacted(true); + mlc.setReceiveTimeout(10); CountDownLatch latch2 = new CountDownLatch(1); mlc.setMessageListener(msg -> { template.convertAndSend("foo", "bar"); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerManualAckIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerManualAckIntegrationTests.java index 9eb3fe9902..c65a2008ec 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerManualAckIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerManualAckIntegrationTests.java @@ -154,6 +154,7 @@ private SimpleMessageListenerContainer createContainer(Object listener) { container.setConcurrentConsumers(concurrentConsumers); container.setChannelTransacted(transactional); container.setAcknowledgeMode(AcknowledgeMode.MANUAL); + container.setReceiveTimeout(10); container.afterPropertiesSet(); container.start(); return container; diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerTxSizeIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerTxSizeIntegrationTests.java index adc1332cd5..7a9df1b488 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerTxSizeIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerTxSizeIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -132,6 +132,7 @@ private SimpleMessageListenerContainer createContainer(Object listener) { container.setConcurrentConsumers(concurrentConsumers); container.setChannelTransacted(transactional); container.setAcknowledgeMode(AcknowledgeMode.AUTO); + container.setReceiveTimeout(10); container.afterPropertiesSet(); container.start(); return container; @@ -139,7 +140,7 @@ private SimpleMessageListenerContainer createContainer(Object listener) { public class TestListener implements ChannelAwareMessageListener { - private final ThreadLocal count = new ThreadLocal(); + private final ThreadLocal count = new ThreadLocal<>(); private final CountDownLatch latch; @@ -174,6 +175,7 @@ public void onMessage(Message message, Channel channel) throws Exception { latch.countDown(); } } + } } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/QueueDeclarationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/QueueDeclarationTests.java index 2c99f8a8b0..b963918c1a 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/QueueDeclarationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/QueueDeclarationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2023-2024 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. @@ -116,6 +116,7 @@ private SimpleMessageListenerContainer createContainer(AmqpAdmin admin, final Co SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); container.setQueueNames("test"); container.setPrefetchCount(2); + container.setReceiveTimeout(10); container.setAmqpAdmin(admin); container.afterPropertiesSet(); return container; diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java index ec13130380..4e3d4f6546 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -320,6 +320,7 @@ public void testListenFromAnonQueue() throws Exception { container.setMessageListener(new MessageListenerAdapter(new PojoListener(latch))); container.setQueueNames(queue.getName()); container.setConcurrentConsumers(2); + container.setReceiveTimeout(10); GenericApplicationContext context = new GenericApplicationContext(); context.getBeanFactory().registerSingleton("foo", queue); context.refresh(); @@ -354,6 +355,7 @@ public void testExclusive() throws Exception { new SimpleMessageListenerContainer(template.getConnectionFactory()); container1.setMessageListener(new MessageListenerAdapter(new PojoListener(latch1))); container1.setQueueNames(queue.getName()); + container1.setReceiveTimeout(10); GenericApplicationContext context = new GenericApplicationContext(); context.getBeanFactory().registerSingleton("foo", queue); context.refresh(); @@ -376,6 +378,7 @@ public void testExclusive() throws Exception { container2.setQueueNames(queue.getName()); container2.setApplicationContext(context); container2.setRecoveryInterval(1000); + container2.setReceiveTimeout(10); container2.setExclusive(true); // not really necessary, but likely people will make all consumers exclusive. final AtomicReference eventRef = new AtomicReference<>(); final CountDownLatch consumeLatch2 = new CountDownLatch(1); @@ -466,6 +469,7 @@ public void basicQos(int prefetchCount, boolean global) throws IOException { container.setQueueNames(queue.getName()); container.setRecoveryInterval(500); container.setGlobalQos(true); + container.setReceiveTimeout(10); container.afterPropertiesSet(); container.start(); @@ -517,6 +521,7 @@ public DeclareOk queueDeclarePassive(String queue) throws IOException { container.setMessageListener(new MessageListenerAdapter(new PojoListener(latch))); container.setQueueNames(queue.getName()); container.setRecoveryInterval(500); + container.setReceiveTimeout(10); container.afterPropertiesSet(); container.start(); @@ -544,6 +549,7 @@ public void testRestartConsumerMissingQueue() throws Exception { container.setDeclarationRetries(1); container.setFailedDeclarationRetryInterval(100); container.setRetryDeclarationInterval(30000); + container.setReceiveTimeout(10); container.setApplicationEventPublisher(event -> { if (event instanceof MissingQueueEvent) { missingLatch.countDown(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerLongTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerLongTests.java index 81faf4f84e..0a0a1641c9 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerLongTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerLongTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -26,7 +26,6 @@ import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.Test; -import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.core.Queue; import org.springframework.amqp.rabbit.connection.SingleConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitAdmin; @@ -90,6 +89,7 @@ private void testChangeConsumerCountGuts(boolean transacted) throws Exception { container.setAutoStartup(false); container.setConcurrentConsumers(2); container.setChannelTransacted(transacted); + container.setReceiveTimeout(10); container.afterPropertiesSet(); assertThat(ReflectionTestUtils.getField(container, "concurrentConsumers")).isEqualTo(2); container.start(); @@ -115,11 +115,12 @@ private void testChangeConsumerCountGuts(boolean transacted) throws Exception { } @Test - public void testAddQueuesAndStartInCycle() throws Exception { + public void testAddQueuesAndStartInCycle() { final SimpleMessageListenerContainer container = new SimpleMessageListenerContainer( this.connectionFactory); - container.setMessageListener((MessageListener) message -> { }); + container.setMessageListener(message -> { }); container.setConcurrentConsumers(2); + container.setReceiveTimeout(10); container.afterPropertiesSet(); RabbitAdmin admin = new RabbitAdmin(this.connectionFactory); @@ -145,6 +146,7 @@ public void testIncreaseMinAtMax() throws Exception { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(this.connectionFactory); container.setStartConsumerMinInterval(100); container.setConsecutiveActiveTrigger(1); + container.setReceiveTimeout(10); container.setMessageListener(m -> { try { Thread.sleep(50); @@ -184,6 +186,7 @@ public void testDecreaseMinAtMax() throws Exception { container.setQueueNames(QUEUE3); container.setConcurrentConsumers(2); container.setMaxConcurrentConsumers(3); + container.setReceiveTimeout(10); container.afterPropertiesSet(); container.start(); RabbitTemplate template = new RabbitTemplate(this.connectionFactory); @@ -212,6 +215,7 @@ public void testDecreaseMaxAtMax() throws Exception { container.setQueueNames(QUEUE4); container.setConcurrentConsumers(2); container.setMaxConcurrentConsumers(3); + container.setReceiveTimeout(10); container.afterPropertiesSet(); container.start(); RabbitTemplate template = new RabbitTemplate(this.connectionFactory); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java index 26c72b14e3..f668ba0276 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java @@ -122,6 +122,7 @@ public void testChannelTransactedOverriddenWhenTxManager() { container.setQueueNames("foo"); container.setChannelTransacted(false); container.setTransactionManager(new TestTransactionManager()); + container.setReceiveTimeout(10); container.afterPropertiesSet(); assertThat(TestUtils.getPropertyValue(container, "transactional", Boolean.class)).isTrue(); container.stop(); @@ -137,6 +138,7 @@ public void testInconsistentTransactionConfiguration() { container.setChannelTransacted(false); container.setAcknowledgeMode(AcknowledgeMode.NONE); container.setTransactionManager(new TestTransactionManager()); + container.setReceiveTimeout(10); assertThatIllegalStateException() .isThrownBy(container::afterPropertiesSet); container.stop(); @@ -151,6 +153,7 @@ public void testInconsistentAcknowledgeConfiguration() { container.setQueueNames("foo"); container.setChannelTransacted(true); container.setAcknowledgeMode(AcknowledgeMode.NONE); + container.setReceiveTimeout(10); assertThatIllegalStateException() .isThrownBy(container::afterPropertiesSet); container.stop(); @@ -165,6 +168,7 @@ public void testDefaultConsumerCount() { container.setQueueNames("foo"); container.setAutoStartup(false); container.setShutdownTimeout(0); + container.setReceiveTimeout(10); container.afterPropertiesSet(); assertThat(ReflectionTestUtils.getField(container, "concurrentConsumers")).isEqualTo(1); container.stop(); @@ -215,6 +219,7 @@ public void testTxSizeAcks() throws Exception { final SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); container.setQueueNames("foo"); container.setBatchSize(2); + container.setReceiveTimeout(10); container.setMessageListener(messages::add); container.start(); BasicProperties props = new BasicProperties(); @@ -267,6 +272,7 @@ public void testTxSizeAcksWIthShortSet() throws Exception { final SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); container.setQueueNames("foobar"); container.setBatchSize(2); + container.setReceiveTimeout(10); container.setMessageListener(messages::add); container.setShutdownTimeout(0); container.afterPropertiesSet(); @@ -317,6 +323,7 @@ public void testConsumerArgs() throws Exception { }); container.setConsumerArguments(Collections.singletonMap("x-priority", 10)); container.setShutdownTimeout(0); + container.setReceiveTimeout(10); container.afterPropertiesSet(); container.start(); verify(channel).basicConsume(anyString(), anyBoolean(), anyString(), anyBoolean(), anyBoolean(), @@ -369,6 +376,7 @@ public void testChangeQueuesSimple() { ConnectionFactory connectionFactory = mock(ConnectionFactory.class); final SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); container.setQueueNames("foo"); + container.setReceiveTimeout(10); List queues = TestUtils.getPropertyValue(container, "queues", List.class); assertThat(queues).hasSize(1); container.addQueueNames(new AnonymousQueue().getName(), new AnonymousQueue().getName()); @@ -400,6 +408,7 @@ public void testAddQueuesAndStartInCycle() throws Exception { container.setMessageListener(message -> { }); container.setShutdownTimeout(0); + container.setReceiveTimeout(10); container.afterPropertiesSet(); for (int i = 0; i < 10; i++) { @@ -487,6 +496,7 @@ public void testWithConnectionPerListenerThread() throws Exception { container.setConcurrentConsumers(2); container.setQueueNames("foo"); container.setConsumeDelay(100); + container.setReceiveTimeout(10); container.afterPropertiesSet(); CountDownLatch latch1 = new CountDownLatch(2); @@ -519,7 +529,7 @@ public void testWithConnectionPerListenerThread() throws Exception { waitForConsumersToStop(consumers); Set allocatedConnections = TestUtils.getPropertyValue(ccf, "allocatedConnections", Set.class); assertThat(allocatedConnections).hasSize(2); - assertThat(ccf.getCacheProperties().get("openConnections")).isEqualTo("2"); + assertThat(ccf.getCacheProperties().get("openConnections")).isEqualTo("1"); } @Test @@ -597,6 +607,7 @@ public void testPossibleAuthenticationFailureNotFatal() { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); container.setConnectionFactory(connectionFactory); container.setQueueNames("foo"); + container.setReceiveTimeout(10); container.setPossibleAuthenticationFailureFatal(false); container.start(); @@ -745,6 +756,7 @@ void filterMppNoDoubleAck() throws Exception { container.setMessageListener(listener); container.setBatchSize(2); container.setConsumerBatchEnabled(true); + container.setReceiveTimeout(10); container.start(); BasicProperties props = new BasicProperties(); byte[] payload = "baz".getBytes(); @@ -827,7 +839,7 @@ public void testBatchReceiveTimedOut() throws Exception { Thread.sleep(20); envelope = new Envelope(3L, false, "foo", "bar"); consumer.get().handleDelivery("1", envelope, props, payload); - assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); verify(channel, never()).basicAck(eq(1), anyBoolean()); verify(channel).basicAck(2, true); verify(channel, never()).basicAck(eq(2), anyBoolean()); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerWithRabbitMQ.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerWithRabbitMQ.java index ad3d808d7d..1de168c5e4 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerWithRabbitMQ.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerWithRabbitMQ.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -63,6 +63,7 @@ public static void main(String[] args) throws InterruptedException { container.setBatchSize(500); container.setAcknowledgeMode(AcknowledgeMode.AUTO); container.setConcurrentConsumers(20); + container.setReceiveTimeout(10); container.setMessageListener(new MessageListenerAdapter(new SimpleAdapter(), messageConverter)); container.start(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderConfiguration.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderConfiguration.java index 2e46057088..17275801e4 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderConfiguration.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -106,6 +106,7 @@ public SimpleMessageListenerContainer listenerContainer() { // container.setMessageListener(testListener(4)); container.setAutoStartup(false); container.setAcknowledgeMode(AcknowledgeMode.AUTO); + container.setReceiveTimeout(10); return container; } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/MissingIdRetryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/MissingIdRetryTests.java index eef04a3a71..860a4af405 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/MissingIdRetryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/MissingIdRetryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -96,6 +96,7 @@ public void testWithNoId() throws Exception { container.setStatefulRetryFatalWithNullMessageId(false); container.setMessageListener(new MessageListenerAdapter(new POJO())); container.setQueueNames("retry.test.queue"); + container.setReceiveTimeout(10); StatefulRetryOperationsInterceptorFactoryBean fb = new StatefulRetryOperationsInterceptorFactoryBean(); @@ -134,6 +135,7 @@ public void testWithId() throws Exception { ConnectionFactory connectionFactory = ctx.getBean(ConnectionFactory.class); SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); container.setPrefetchCount(1); + container.setReceiveTimeout(10); container.setMessageListener(new MessageListenerAdapter(new POJO())); container.setQueueNames("retry.test.queue"); @@ -197,6 +199,7 @@ public void testWithIdAndSuccess() throws Exception { } }); container.setQueueNames("retry.test.queue"); + container.setReceiveTimeout(10); StatefulRetryOperationsInterceptorFactoryBean fb = new StatefulRetryOperationsInterceptorFactoryBean(); @@ -221,7 +224,7 @@ public void testWithIdAndSuccess() throws Exception { try { assertThat(cdl.await(30, TimeUnit.SECONDS)).isTrue(); Map map = (Map) new DirectFieldAccessor(cache).getPropertyValue("map"); - await().until(() -> map.size() == 0); + await().until(map::isEmpty); ArgumentCaptor putCaptor = ArgumentCaptor.forClass(Object.class); ArgumentCaptor getCaptor = ArgumentCaptor.forClass(Object.class); ArgumentCaptor removeCaptor = ArgumentCaptor.forClass(Object.class); diff --git a/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt b/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt index 8eadd20341..ea320a0567 100644 --- a/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt +++ b/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * Copyright 2018-2024 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. @@ -93,6 +93,7 @@ class EnableRabbitKotlinTests { fun rabbitListenerContainerFactory(cf: CachingConnectionFactory) = SimpleRabbitListenerContainerFactory().also { it.setAcknowledgeMode(AcknowledgeMode.MANUAL) + it.setReceiveTimeout(10) it.setConnectionFactory(cf) } From e7be534b547e4790640e74efdf3fdafa28cd2642 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 6 Feb 2024 13:37:19 -0500 Subject: [PATCH 375/737] GH-2609: Restore ClassLoader for SerMC & SimpleMC Fixes: #2609 The deserialization functionality must rely on the `ClassLoader` from the application context (at least, by default). * Fix `SimpleMessageConverter` to accept `BeanClassLoaderAware` and use it for `ConfigurableObjectInputStream` * Fix `SerializerMessageConverter` to accept `BeanClassLoaderAware` * Remove reflection for the `new DirectFieldAccessor(deserializer).getPropertyValue("classLoader")` from the `SerializerMessageConverter` since it is not this converter responsibility to interfere into provided `Deserializer` logic **Cherry-pick to `3.0.x`** --- .../converter/SerializerMessageConverter.java | 69 ++++++++----------- .../converter/SimpleMessageConverter.java | 45 +++++++----- .../SerializerMessageConverterTests.java | 27 +------- 3 files changed, 58 insertions(+), 83 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SerializerMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SerializerMessageConverter.java index bdbe2bde0d..cfe8e923d5 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SerializerMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SerializerMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -26,13 +26,14 @@ import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; -import org.springframework.beans.DirectFieldAccessor; +import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.core.ConfigurableObjectInputStream; import org.springframework.core.serializer.DefaultDeserializer; import org.springframework.core.serializer.DefaultSerializer; import org.springframework.core.serializer.Deserializer; import org.springframework.core.serializer.Serializer; import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; /** * Implementation of {@link MessageConverter} that can work with Strings or native objects @@ -50,26 +51,26 @@ * @author Gary Russell * @author Artem Bilan */ -public class SerializerMessageConverter extends AllowedListDeserializingMessageConverter { +public class SerializerMessageConverter extends AllowedListDeserializingMessageConverter + implements BeanClassLoaderAware { public static final String DEFAULT_CHARSET = StandardCharsets.UTF_8.name(); - private volatile String defaultCharset = DEFAULT_CHARSET; + private String defaultCharset = DEFAULT_CHARSET; - private volatile Serializer serializer = new DefaultSerializer(); + private Serializer serializer = new DefaultSerializer(); - private volatile Deserializer deserializer = new DefaultDeserializer(); + private Deserializer deserializer = new DefaultDeserializer(); - private volatile boolean ignoreContentType = false; + private boolean ignoreContentType = false; - private volatile ClassLoader defaultDeserializerClassLoader; + private ClassLoader defaultDeserializerClassLoader = ClassUtils.getDefaultClassLoader(); - private volatile boolean usingDefaultDeserializer = true; + private boolean usingDefaultDeserializer = true; /** * Flag to signal that the content type should be ignored and the deserializer used irrespective if it is a text * message. Defaults to false, in which case the default encoding is used to convert a text message to a String. - * * @param ignoreContentType the flag value to set */ public void setIgnoreContentType(boolean ignoreContentType) { @@ -79,7 +80,6 @@ public void setIgnoreContentType(boolean ignoreContentType) { /** * Specify the default charset to use when converting to or from text-based Message body content. If not specified, * the charset will be "UTF-8". - * * @param defaultCharset The default charset. */ public void setDefaultCharset(@Nullable String defaultCharset) { @@ -88,7 +88,6 @@ public void setDefaultCharset(@Nullable String defaultCharset) { /** * The serializer to use for converting Java objects to message bodies. - * * @param serializer the serializer to set */ public void setSerializer(Serializer serializer) { @@ -97,24 +96,16 @@ public void setSerializer(Serializer serializer) { /** * The deserializer to use for converting from message body to Java object. - * * @param deserializer the deserializer to set */ public void setDeserializer(Deserializer deserializer) { this.deserializer = deserializer; - if (this.deserializer.getClass().equals(DefaultDeserializer.class)) { - try { - this.defaultDeserializerClassLoader = (ClassLoader) new DirectFieldAccessor(deserializer) - .getPropertyValue("classLoader"); - } - catch (Exception e) { - // no-op - } - this.usingDefaultDeserializer = true; - } - else { - this.usingDefaultDeserializer = false; - } + this.usingDefaultDeserializer = false; + } + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.defaultDeserializerClassLoader = classLoader; } /** @@ -170,17 +161,17 @@ private Object asString(Message message, MessageProperties properties) { private Object deserialize(ByteArrayInputStream inputStream) throws IOException { try (ObjectInputStream objectInputStream = new ConfigurableObjectInputStream(inputStream, - this.defaultDeserializerClassLoader) { - - @Override - protected Class resolveClass(ObjectStreamClass classDesc) - throws IOException, ClassNotFoundException { - Class clazz = super.resolveClass(classDesc); - checkAllowedList(clazz); - return clazz; - } + this.defaultDeserializerClassLoader) { + + @Override + protected Class resolveClass(ObjectStreamClass classDesc) + throws IOException, ClassNotFoundException { + Class clazz = super.resolveClass(classDesc); + checkAllowedList(clazz); + return clazz; + } - }) { + }) { return objectInputStream.readObject(); } catch (ClassNotFoundException ex) { @@ -194,6 +185,7 @@ protected Class resolveClass(ObjectStreamClass classDesc) @Override protected Message createMessage(Object object, MessageProperties messageProperties) throws MessageConversionException { + byte[] bytes; if (object instanceof String) { try { @@ -220,9 +212,8 @@ else if (object instanceof byte[]) { bytes = output.toByteArray(); messageProperties.setContentType(MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT); } - if (bytes != null) { - messageProperties.setContentLength(bytes.length); - } + + messageProperties.setContentLength(bytes.length); return new Message(bytes, messageProperties); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SimpleMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SimpleMessageConverter.java index d162147d0c..56897f80fb 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SimpleMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SimpleMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -27,6 +27,10 @@ import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.utils.SerializationUtils; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.core.ConfigurableObjectInputStream; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; /** * Implementation of {@link MessageConverter} that can work with Strings, Serializable @@ -38,23 +42,30 @@ * @author Mark Fisher * @author Oleg Zhurakousky * @author Gary Russell + * @author Artem Bilan */ -public class SimpleMessageConverter extends AllowedListDeserializingMessageConverter { +public class SimpleMessageConverter extends AllowedListDeserializingMessageConverter implements BeanClassLoaderAware { public static final String DEFAULT_CHARSET = "UTF-8"; - private volatile String defaultCharset = DEFAULT_CHARSET; + private String defaultCharset = DEFAULT_CHARSET; + + private ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); /** * Specify the default charset to use when converting to or from text-based * Message body content. If not specified, the charset will be "UTF-8". - * * @param defaultCharset The default charset. */ - public void setDefaultCharset(String defaultCharset) { + public void setDefaultCharset(@Nullable String defaultCharset) { this.defaultCharset = (defaultCharset != null) ? defaultCharset : DEFAULT_CHARSET; } + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + /** * Converts from a AMQP Message to an Object. */ @@ -73,8 +84,7 @@ public Object fromMessage(Message message) throws MessageConversionException { content = new String(message.getBody(), encoding); } catch (UnsupportedEncodingException e) { - throw new MessageConversionException( - "failed to convert text-based Message content", e); + throw new MessageConversionException("failed to convert text-based Message content", e); } } else if (contentType != null && @@ -84,8 +94,7 @@ else if (contentType != null && createObjectInputStream(new ByteArrayInputStream(message.getBody()))); } catch (IOException | IllegalArgumentException | IllegalStateException e) { - throw new MessageConversionException( - "failed to convert serialized Message content", e); + throw new MessageConversionException("failed to convert serialized Message content", e); } } } @@ -99,7 +108,9 @@ else if (contentType != null && * Creates an AMQP Message from the provided Object. */ @Override - protected Message createMessage(Object object, MessageProperties messageProperties) throws MessageConversionException { + protected Message createMessage(Object object, MessageProperties messageProperties) + throws MessageConversionException { + byte[] bytes = null; if (object instanceof byte[]) { bytes = (byte[]) object; @@ -110,8 +121,7 @@ else if (object instanceof String) { bytes = ((String) object).getBytes(this.defaultCharset); } catch (UnsupportedEncodingException e) { - throw new MessageConversionException( - "failed to convert to Message content", e); + throw new MessageConversionException("failed to convert to Message content", e); } messageProperties.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN); messageProperties.setContentEncoding(this.defaultCharset); @@ -121,8 +131,7 @@ else if (object instanceof Serializable) { bytes = SerializationUtils.serialize(object); } catch (IllegalArgumentException e) { - throw new MessageConversionException( - "failed to convert to serialized Message content", e); + throw new MessageConversionException("failed to convert to serialized Message content", e); } messageProperties.setContentType(MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT); } @@ -135,15 +144,15 @@ else if (object instanceof Serializable) { } /** - * Create an ObjectInputStream for the given InputStream and codebase. The default - * implementation creates an ObjectInputStream. + * Create an ObjectInputStream for the given InputStream. The default + * implementation creates an {@link ConfigurableObjectInputStream} against configured {@link ClassLoader}. + * The class for object to deserialize is checked against {@code allowedListPatterns}. * @param is the InputStream to read from * @return the new ObjectInputStream instance to use * @throws IOException if creation of the ObjectInputStream failed */ - @SuppressWarnings("deprecation") protected ObjectInputStream createObjectInputStream(InputStream is) throws IOException { - return new ObjectInputStream(is) { + return new ConfigurableObjectInputStream(is, this.classLoader) { @Override protected Class resolveClass(ObjectStreamClass classDesc) throws IOException, ClassNotFoundException { diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/SerializerMessageConverterTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/SerializerMessageConverterTests.java index 744adb5748..026fe7a308 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/SerializerMessageConverterTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/SerializerMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -18,25 +18,18 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; -import org.springframework.amqp.utils.test.TestUtils; -import org.springframework.core.serializer.DefaultDeserializer; -import org.springframework.core.serializer.Deserializer; /** * @author Mark Fisher @@ -151,24 +144,6 @@ public void serializedObjectToMessage() throws Exception { assertThat(deserializedObject).isEqualTo(testBean); } - @SuppressWarnings("unchecked") - @Test - public void testDefaultDeserializerClassLoader() throws Exception { - SerializerMessageConverter converter = new SerializerMessageConverter(); - ClassLoader loader = mock(ClassLoader.class); - Deserializer deserializer = new DefaultDeserializer(loader); - converter.setDeserializer(deserializer); - assertThat(TestUtils.getPropertyValue(converter, "defaultDeserializerClassLoader")).isSameAs(loader); - assertThat(TestUtils.getPropertyValue(converter, "usingDefaultDeserializer", Boolean.class)).isTrue(); - Deserializer mock = mock(Deserializer.class); - converter.setDeserializer(mock); - assertThat(TestUtils.getPropertyValue(converter, "usingDefaultDeserializer", Boolean.class)).isFalse(); - TestBean testBean = new TestBean("foo"); - Message message = converter.toMessage(testBean, new MessageProperties()); - converter.fromMessage(message); - verify(mock).deserialize(Mockito.any(InputStream.class)); - } - @Test public void messageConversionExceptionForClassNotFound() { SerializerMessageConverter converter = new SerializerMessageConverter(); From ffce75476a7c2f1a90d1cf99c7d9135c6dc9db5c Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 7 Feb 2024 17:43:43 -0500 Subject: [PATCH 376/737] Upgrade to the latest reusable workflows --- .github/workflows/backport-issue.yml | 2 +- .github/workflows/ci-snapshot.yml | 5 +++-- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/merge-dependabot-pr.yml | 4 +++- .github/workflows/pr-build.yml | 2 +- .github/workflows/release.yml | 6 ++++-- 6 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/backport-issue.yml b/.github/workflows/backport-issue.yml index c46f500e15..ae3ea05130 100644 --- a/.github/workflows/backport-issue.yml +++ b/.github/workflows/backport-issue.yml @@ -7,6 +7,6 @@ on: jobs: backport-issue: - uses: spring-io/spring-github-workflows/.github/workflows/spring-backport-issue.yml@v2 + uses: spring-io/spring-github-workflows/.github/workflows/spring-backport-issue.yml@main secrets: GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/ci-snapshot.yml b/.github/workflows/ci-snapshot.yml index ac1da95ad1..98e223d8ca 100644 --- a/.github/workflows/ci-snapshot.yml +++ b/.github/workflows/ci-snapshot.yml @@ -17,11 +17,12 @@ concurrency: jobs: build-snapshot: - uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-snapshot.yml@v2 + uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-snapshot.yml@main with: gradleTasks: ${{ github.event_name == 'schedule' && '--rerun-tasks' || '' }} secrets: GRADLE_ENTERPRISE_CACHE_USER: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} GRADLE_ENTERPRISE_SECRET_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} - JF_ARTIFACTORY_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} \ No newline at end of file + ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} + ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} \ No newline at end of file diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 54d562340b..1771c58265 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -16,4 +16,4 @@ permissions: jobs: dispatch-docs-build: if: github.repository_owner == 'spring-projects' - uses: spring-io/spring-github-workflows/.github/workflows/spring-dispatch-docs-build.yml@v2 + uses: spring-io/spring-github-workflows/.github/workflows/spring-dispatch-docs-build.yml@main diff --git a/.github/workflows/merge-dependabot-pr.yml b/.github/workflows/merge-dependabot-pr.yml index 2e5a3c261f..da9e0bf656 100644 --- a/.github/workflows/merge-dependabot-pr.yml +++ b/.github/workflows/merge-dependabot-pr.yml @@ -11,4 +11,6 @@ jobs: merge-dependabot-pr: permissions: write-all - uses: spring-io/spring-github-workflows/.github/workflows/spring-merge-dependabot-pr.yml@v2 \ No newline at end of file + uses: spring-io/spring-github-workflows/.github/workflows/spring-merge-dependabot-pr.yml@main + with: + mergeArguments: --auto --squash \ No newline at end of file diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 73821067fc..e5457892cc 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -7,4 +7,4 @@ on: jobs: build-pull-request: - uses: spring-io/spring-github-workflows/.github/workflows/spring-gradle-pull-request-build.yml@v2 + uses: spring-io/spring-github-workflows/.github/workflows/spring-gradle-pull-request-build.yml@main diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fc09479e40..ea7ee529b2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,17 +12,19 @@ jobs: contents: write issues: write - uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-release.yml@v2 + uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-release.yml@main secrets: GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} GRADLE_ENTERPRISE_CACHE_USER: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} GRADLE_ENTERPRISE_SECRET_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} JF_ARTIFACTORY_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} + ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} + ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} OSSRH_URL: ${{ secrets.OSSRH_URL }} OSSRH_S01_TOKEN_USERNAME: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }} OSSRH_S01_TOKEN_PASSWORD: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }} OSSRH_STAGING_PROFILE_NAME: ${{ secrets.OSSRH_STAGING_PROFILE_NAME }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} - SPRING_RELEASE_SLACK_WEBHOOK_URL: ${{ secrets.SPRING_RELEASE_SLACK_WEBHOOK_URL }} \ No newline at end of file +# SPRING_RELEASE_SLACK_WEBHOOK_URL: ${{ secrets.SPRING_RELEASE_SLACK_WEBHOOK_URL }} \ No newline at end of file From c967f8d089b06714705942c9d6ca23f996eacb40 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 7 Feb 2024 18:29:54 -0500 Subject: [PATCH 377/737] Re-enable Slack notification for release workflow --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ea7ee529b2..fd2fbeb864 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,4 +27,4 @@ jobs: OSSRH_STAGING_PROFILE_NAME: ${{ secrets.OSSRH_STAGING_PROFILE_NAME }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} -# SPRING_RELEASE_SLACK_WEBHOOK_URL: ${{ secrets.SPRING_RELEASE_SLACK_WEBHOOK_URL }} \ No newline at end of file + SPRING_RELEASE_SLACK_WEBHOOK_URL: ${{ secrets.SPRING_RELEASE_SLACK_WEBHOOK_URL }} \ No newline at end of file From 052599582baea829edb8893ec645f74b2752f050 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 9 Feb 2024 12:56:32 -0500 Subject: [PATCH 378/737] Upgrade to Gradle 8.6 * Add `package-ecosystem: "github-actions"` into Dependabot config --- .github/dependabot.yml | 12 ++++++++++++ gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 91d3d1e368..0830d7a16b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -27,3 +27,15 @@ updates: - "org.xerial.snappy:snappy-java" - "org.lz4:lz4-java" - "com.github.luben:zstd-jni" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "saturday" + ignore: + - dependency-name: "*" + groups: + development-dependencies: + patterns: + - "*" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 46671acb6e..4baf5a11d4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=3e1af3ae886920c3ac87f7a91f816c0c7c436f276a6eefdb3da152100fef72ae -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionSha256Sum=9631d53cf3e74bfa726893aee1f8994fee4e060c401335946dba2156f440f24c +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 190dd7e362d07b9a2c10eb65ef52c79c74261346 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 Feb 2024 18:00:09 +0000 Subject: [PATCH 379/737] Bump org.testcontainers:testcontainers-bom from 1.19.4 to 1.19.5 (#2611) Bumps [org.testcontainers:testcontainers-bom](https://github.com/testcontainers/testcontainers-java) from 1.19.4 to 1.19.5. - [Release notes](https://github.com/testcontainers/testcontainers-java/releases) - [Changelog](https://github.com/testcontainers/testcontainers-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/testcontainers/testcontainers-java/compare/1.19.4...1.19.5) --- updated-dependencies: - dependency-name: org.testcontainers:testcontainers-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 60045fec27..6e12fd3c7a 100644 --- a/build.gradle +++ b/build.gradle @@ -71,7 +71,7 @@ ext { springDataVersion = '2023.1.2' springRetryVersion = '2.0.5' springVersion = '6.1.3' - testcontainersVersion = '1.19.4' + testcontainersVersion = '1.19.5' zstdJniVersion = '1.5.5-11' javaProjects = subprojects - project(':spring-amqp-bom') From 3c1715f00585424613b6aa9c7e0409e6ded02a85 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 Feb 2024 18:00:23 +0000 Subject: [PATCH 380/737] Bump org.junit:junit-bom from 5.10.1 to 5.10.2 (#2612) Bumps [org.junit:junit-bom](https://github.com/junit-team/junit5) from 5.10.1 to 5.10.2. - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.10.1...r5.10.2) --- updated-dependencies: - dependency-name: org.junit:junit-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 6e12fd3c7a..673ff38ea0 100644 --- a/build.gradle +++ b/build.gradle @@ -55,7 +55,7 @@ ext { jacksonBomVersion = '2.15.3' jaywayJsonPathVersion = '2.8.0' junit4Version = '4.13.2' - junitJupiterVersion = '5.10.1' + junitJupiterVersion = '5.10.2' kotlinCoroutinesVersion = '1.7.3' log4jVersion = '2.21.1' logbackVersion = '1.4.14' From 153a85c42ab8945ec3dcc323509c6adc2e9d89c7 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 9 Feb 2024 13:07:37 -0500 Subject: [PATCH 381/737] Fix Dependabot config for GHAs --- .github/dependabot.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0830d7a16b..2b3dc0d76a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -33,9 +33,4 @@ updates: schedule: interval: "weekly" day: "saturday" - ignore: - - dependency-name: "*" - groups: - development-dependencies: - patterns: - - "*" + labels: ["type: task"] From 7ea6ef3ad379db01162728484db4e46f1252afb3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 Feb 2024 18:10:34 +0000 Subject: [PATCH 382/737] Bump actions/upload-artifact from 3 to 4 (#2614) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/verify-staged-artifacts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verify-staged-artifacts.yml b/.github/workflows/verify-staged-artifacts.yml index 58ee4f83e8..1d217b324f 100644 --- a/.github/workflows/verify-staged-artifacts.yml +++ b/.github/workflows/verify-staged-artifacts.yml @@ -48,7 +48,7 @@ jobs: - name: Capture Test Results if: failure() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: test-results path: '**/target/surefire-reports/**/*.*' From 7cb20c715318eaeae523a6d85cdac3a40b1a4628 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 9 Feb 2024 13:15:53 -0500 Subject: [PATCH 383/737] Use group for GHAs in Dependabot config --- .github/dependabot.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2b3dc0d76a..78800b6752 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -34,3 +34,7 @@ updates: interval: "weekly" day: "saturday" labels: ["type: task"] + groups: + development-dependencies: + patterns: + - "*" From 27f46fa51c85d73c0becd9ae45518e47c232bcd0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 Feb 2024 18:17:21 +0000 Subject: [PATCH 384/737] Bump the development-dependencies group with 2 updates (#2616) Bumps the development-dependencies group with 2 updates: [actions/setup-java](https://github.com/actions/setup-java) and [jfrog/setup-jfrog-cli](https://github.com/jfrog/setup-jfrog-cli). Updates `actions/setup-java` from 3 to 4 - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/v3...v4) Updates `jfrog/setup-jfrog-cli` from 3 to 4 - [Release notes](https://github.com/jfrog/setup-jfrog-cli/releases) - [Commits](https://github.com/jfrog/setup-jfrog-cli/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/setup-java dependency-type: direct:production update-type: version-update:semver-major dependency-group: development-dependencies - dependency-name: jfrog/setup-jfrog-cli dependency-type: direct:production update-type: version-update:semver-major dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/verify-staged-artifacts.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/verify-staged-artifacts.yml b/.github/workflows/verify-staged-artifacts.yml index 1d217b324f..d013118a08 100644 --- a/.github/workflows/verify-staged-artifacts.yml +++ b/.github/workflows/verify-staged-artifacts.yml @@ -30,13 +30,13 @@ jobs: show-progress: false - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 cache: 'maven' - - uses: jfrog/setup-jfrog-cli@v3 + - uses: jfrog/setup-jfrog-cli@v4 env: JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} From 8395d93c0a0b9bacd82c5ead927b268e865750c8 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 9 Feb 2024 13:22:55 -0500 Subject: [PATCH 385/737] Add `auto-cherry-pick` GHA workflow --- .github/workflows/auto-cherry-pick.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/workflows/auto-cherry-pick.yml diff --git a/.github/workflows/auto-cherry-pick.yml b/.github/workflows/auto-cherry-pick.yml new file mode 100644 index 0000000000..4a9c4479ef --- /dev/null +++ b/.github/workflows/auto-cherry-pick.yml @@ -0,0 +1,13 @@ +name: Auto Cherry-Pick + +on: + push: + branches: + - main + - '*.x' + +jobs: + cherry-pick-commit: + uses: spring-io/spring-github-workflows/.github/workflows/spring-cherry-pick.yml@main + secrets: + GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} \ No newline at end of file From f0a0b3482f76e4cae709e86a7a3270cdd408f1f5 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Sun, 11 Feb 2024 11:06:48 -0500 Subject: [PATCH 386/737] Try to narrow Dependabot registries --- .github/dependabot.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 78800b6752..96e34b519a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,7 +1,17 @@ version: 2 +registries: + maven-central: + type: maven-repository + url: https://repo.maven.apache.org/maven2 + spring-milestones: + type: maven-repository + url: https://repo.spring.io/milestone updates: - package-ecosystem: "gradle" directory: "/" + registries: + - maven-central + - spring-milestones schedule: interval: "weekly" day: "saturday" From 61e7a5e46e7fb4a8ffa6ebb79a2cd869768c6677 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 12 Feb 2024 09:30:14 -0500 Subject: [PATCH 387/737] Revert "Try to narrow Dependabot registries" This reverts commit f0a0b3482f76e4cae709e86a7a3270cdd408f1f5. --- .github/dependabot.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 96e34b519a..78800b6752 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,17 +1,7 @@ version: 2 -registries: - maven-central: - type: maven-repository - url: https://repo.maven.apache.org/maven2 - spring-milestones: - type: maven-repository - url: https://repo.spring.io/milestone updates: - package-ecosystem: "gradle" directory: "/" - registries: - - maven-central - - spring-milestones schedule: interval: "weekly" day: "saturday" From 95d292a297145c00f8a6bec2eb35023b1eaa1cb9 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 14 Feb 2024 15:05:15 -0500 Subject: [PATCH 388/737] Fix NPE for `RabbitAccessor.observationRegistry` When observation is enabled on component, but no `ObservationRegistry` bean in the application context, we must fall back to the default one `ObservationRegistry.NOOP` **Auto-cherry-pick to `3.0.x`** --- .../amqp/rabbit/connection/RabbitAccessor.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java index 88d7d9b65b..6c91c630f7 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -33,6 +33,7 @@ * @author Mark Fisher * @author Dave Syer * @author Gary Russell + * @author Artem Bilan */ public abstract class RabbitAccessor implements InitializingBean { @@ -122,7 +123,7 @@ protected void obtainObservationRegistry(@Nullable ApplicationContext appContext if (appContext != null) { ObjectProvider registry = appContext.getBeanProvider(ObservationRegistry.class); - this.observationRegistry = registry.getIfUnique(); + this.observationRegistry = registry.getIfUnique(() -> this.observationRegistry); } } From 2ad69c18207baa2198c1410e40acc6d1cf33fbe9 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 15 Feb 2024 12:07:18 -0500 Subject: [PATCH 389/737] GH-2590: RepubMRecoverWithConfirms: SpEL-based ctor Fixes: #2590 Expose the SpEL-based `errorExchange` & `errorRoutingKey` on `RepublishMessageRecovererWithConfirms` like it was done in the `RepublishMessageRecoverer`. * Add `RepublishMessageRecovererWithConfirms(RabbitTemplate errorTemplate, @Nullable Expression errorExchange, @Nullable Expression errorRoutingKey, ConfirmType confirmType)` * Fix typos in `RepublishMessageRecovererWithConfirms` Javadocs * Modify `RepublishMessageRecovererWithConfirmsIntegrationTests.testCorrelatedWithNack()` to use SpEL expressions instead of plain strings --- ...RepublishMessageRecovererWithConfirms.java | 42 ++++++++++++++----- ...RecovererWithConfirmsIntegrationTests.java | 23 +++++++--- 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirms.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirms.java index 65701c0c02..40909ecd69 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirms.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirms.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 the original author or authors. + * Copyright 2021-2024 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. @@ -28,12 +28,15 @@ import org.springframework.amqp.rabbit.core.AmqpNackReceivedException; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; +import org.springframework.expression.Expression; import org.springframework.lang.Nullable; /** * A {@link RepublishMessageRecoverer} supporting publisher confirms and returns. * * @author Gary Russell + * @author Artem Bilan + * * @since 2.3.3 * */ @@ -48,20 +51,20 @@ public class RepublishMessageRecovererWithConfirms extends RepublishMessageRecov private long confirmTimeout = DEFAULT_TIMEOUT; /** - * Use the supplied template to publish the messsage with the provided confirm type. + * Use the supplied template to publish the message with the provided confirm type. * The template and its connection factory must be suitably configured to support the - * confirm type. + * {@code confirm} type. * @param errorTemplate the template. * @param confirmType the confirmType. */ public RepublishMessageRecovererWithConfirms(RabbitTemplate errorTemplate, ConfirmType confirmType) { - this(errorTemplate, null, null, confirmType); + this(errorTemplate, (Expression) null, null, confirmType); } /** - * Use the supplied template to publish the messsage with the provided confirm type to + * Use the supplied template to publish the message with the provided confirm type to * the provided exchange with the default routing key. The template and its connection - * factory must be suitably configured to support the confirm type. + * factory must be suitably configured to support the {@code confirm} type. * @param errorTemplate the template. * @param confirmType the confirmType. * @param errorExchange the exchange. @@ -73,9 +76,9 @@ public RepublishMessageRecovererWithConfirms(RabbitTemplate errorTemplate, Strin } /** - * Use the supplied template to publish the messsage with the provided confirm type to + * Use the supplied template to publish the message with the provided confirm type to * the provided exchange with the provided routing key. The template and its - * connection factory must be suitably configured to support the confirm type. + * connection factory must be suitably configured to support the {@code confirm} type. * @param errorTemplate the template. * @param confirmType the confirmType. * @param errorExchange the exchange. @@ -90,7 +93,25 @@ public RepublishMessageRecovererWithConfirms(RabbitTemplate errorTemplate, Strin } /** - * Set the confirm timeout; default 10 seconds. + * Use the supplied template to publish the message with the provided confirm type to + * the provided exchange with the provided routing key. The template and its + * connection factory must be suitably configured to support the {@code confirm} type. + * @param errorTemplate the template. + * @param confirmType the confirmType. + * @param errorExchange the exchange. + * @param errorRoutingKey the routing key. + * @since 3.1.2 + */ + public RepublishMessageRecovererWithConfirms(RabbitTemplate errorTemplate, @Nullable Expression errorExchange, + @Nullable Expression errorRoutingKey, ConfirmType confirmType) { + + super(errorTemplate, errorExchange, errorRoutingKey); + this.template = errorTemplate; + this.confirmType = confirmType; + } + + /** + * Set the {@code confirm} timeout; default 10 seconds. * @param confirmTimeout the timeout. */ public void setConfirmTimeout(long confirmTimeout) { @@ -98,8 +119,7 @@ public void setConfirmTimeout(long confirmTimeout) { } @Override - protected void doSend(@Nullable - String exchange, String routingKey, Message message) { + protected void doSend(@Nullable String exchange, String routingKey, Message message) { if (ConfirmType.CORRELATED.equals(this.confirmType)) { doSendCorrelated(exchange, routingKey, message); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirmsIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirmsIntegrationTests.java index fff73934ab..0619370493 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirmsIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirmsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * Copyright 2018-2024 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. @@ -35,6 +35,8 @@ import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.junit.RabbitAvailable; import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; +import org.springframework.expression.common.LiteralExpression; +import org.springframework.expression.spel.standard.SpelExpressionParser; /** * @author Gary Russell @@ -106,12 +108,21 @@ void testCorrelatedWithNack() { .maxLength(1L) .overflow(Overflow.rejectPublish) .build(); + admin.deleteQueue(queue.getName()); admin.declareQueue(queue); - RepublishMessageRecovererWithConfirms recoverer = new RepublishMessageRecovererWithConfirms(template, "", - queue.getName(), ConfirmType.CORRELATED); - recoverer.recover(MessageBuilder.withBody("foo".getBytes()).build(), new RuntimeException()); - assertThatExceptionOfType(AmqpNackReceivedException.class).isThrownBy(() -> - recoverer.recover(MessageBuilder.withBody("foo".getBytes()).build(), new RuntimeException())); + + RepublishMessageRecovererWithConfirms recoverer = new RepublishMessageRecovererWithConfirms(template, + new LiteralExpression(""), + new SpelExpressionParser().parseExpression("messageProperties.headers[queueName]"), + ConfirmType.CORRELATED); + + Message message = MessageBuilder.withBody("foo".getBytes()).setHeader("queueName", queue.getName()).build(); + + recoverer.recover(message, new RuntimeException()); + + assertThatExceptionOfType(AmqpNackReceivedException.class) + .isThrownBy(() -> recoverer.recover(message, new RuntimeException())); + admin.deleteQueue(queue.getName()); ccf.destroy(); } From 456a59b9da2f0abec959fbf500968dab78ca0148 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 16 Feb 2024 11:25:08 -0500 Subject: [PATCH 390/737] Make PR & Dependabot WFs for support branches as well **Auto-cherry-pick to `3.0.x`** --- .github/workflows/merge-dependabot-pr.yml | 1 + .github/workflows/pr-build.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/merge-dependabot-pr.yml b/.github/workflows/merge-dependabot-pr.yml index da9e0bf656..743781a0dc 100644 --- a/.github/workflows/merge-dependabot-pr.yml +++ b/.github/workflows/merge-dependabot-pr.yml @@ -4,6 +4,7 @@ on: pull_request: branches: - main + - '*.x' run-name: Merge Dependabot PR ${{ github.ref_name }} diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index e5457892cc..cefa6bf31a 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -4,6 +4,7 @@ on: pull_request: branches: - main + - '*.x' jobs: build-pull-request: From add0dabb2c9a4a9967ef818271f70ec4e8a8ac40 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 16 Feb 2024 11:32:45 -0500 Subject: [PATCH 391/737] Migrate from `findbugs` to `spotbugs` **Auto-cherry-pick to `3.0.x`** --- build.gradle | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 673ff38ea0..7bccbd80fa 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,7 @@ plugins { id 'io.spring.dependency-management' version '1.1.4' apply false id 'org.antora' version '1.0.0' id 'io.spring.antora.generate-antora-yml' version '0.0.1' + id 'com.github.spotbugs' version '6.0.7' } description = 'Spring AMQP' @@ -49,7 +50,6 @@ ext { commonsCompressVersion = '1.20' commonsHttpClientVersion = '5.2.3' commonsPoolVersion = '2.12.0' - googleJsr305Version = '3.0.2' hamcrestVersion = '2.2' hibernateValidationVersion = '8.0.1.Final' jacksonBomVersion = '2.15.3' @@ -202,7 +202,10 @@ configure(javaProjects) { subproject -> // dependencies that are common across all java projects dependencies { - compileOnly "com.google.code.findbugs:jsr305:$googleJsr305Version" + def spotbugsAnnotations = "com.github.spotbugs:spotbugs-annotations:${spotbugs.toolVersion.get()}" + compileOnly spotbugsAnnotations + testCompileOnly spotbugsAnnotations + testImplementation 'org.apache.logging.log4j:log4j-core' testImplementation "org.hamcrest:hamcrest-core:$hamcrestVersion" testImplementation ("org.mockito:mockito-core:$mockitoVersion") { @@ -224,7 +227,6 @@ configure(javaProjects) { subproject -> // To avoid compiler warnings about @API annotations in JUnit code testCompileOnly 'org.apiguardian:apiguardian-api:1.0.0' - testCompileOnly "com.google.code.findbugs:jsr305:$googleJsr305Version" testImplementation 'org.jetbrains.kotlin:kotlin-reflect' testImplementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' From 1b035e32c0dea8836bbaff54a82f0124dc78ab2c Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 16 Feb 2024 12:10:57 -0500 Subject: [PATCH 392/737] Add Dependabot scanning for branch `3.0.x` --- .github/dependabot.yml | 105 ++++++++++++++++++++++++++++++----------- 1 file changed, 78 insertions(+), 27 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 78800b6752..eea47007ce 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,40 +1,91 @@ version: 2 updates: - - package-ecosystem: "gradle" - directory: "/" + - package-ecosystem: gradle + directory: / schedule: - interval: "weekly" - day: "saturday" + interval: weekly + day: saturday ignore: - - dependency-name: "*" - update-types: ["version-update:semver-major", "version-update:semver-minor"] + - dependency-name: '*' + update-types: + - version-update:semver-major + - version-update:semver-minor open-pull-requests-limit: 10 - labels: ["type: dependency-upgrade"] + labels: + - 'type: dependency-upgrade' groups: development-dependencies: - update-types: ["patch"] + update-types: + - patch patterns: - - "com.gradle.enterprise" - - "io.spring.*" - - "org.ajoberstar.grgit" - - "org.antora" - - "io.micrometer:micrometer-docs-generator" - - "com.willowtreeapps.assertk:assertk-jvm" - - "org.hibernate.validator:hibernate-validator" - - "org.apache.httpcomponents.client5:httpclient5" - - "org.awaitility:awaitility" - - "com.google.code.findbugs:jsr305" - - "org.xerial.snappy:snappy-java" - - "org.lz4:lz4-java" - - "com.github.luben:zstd-jni" + - com.gradle.enterprise + - com.github.spotbugs + - io.spring.* + - org.ajoberstar.grgit + - org.antora + - io.micrometer:micrometer-docs-generator + - com.willowtreeapps.assertk:assertk-jvm + - org.hibernate.validator:hibernate-validator + - org.apache.httpcomponents.client5:httpclient5 + - org.awaitility:awaitility + - org.xerial.snappy:snappy-java + - org.lz4:lz4-java + - com.github.luben:zstd-jni - - package-ecosystem: "github-actions" - directory: "/" + - package-ecosystem: gradle + target-branch: 3.0.x + directory: / schedule: - interval: "weekly" - day: "saturday" - labels: ["type: task"] + interval: weekly + day: saturday + ignore: + - dependency-name: '*' + update-types: + - version-update:semver-major + - version-update:semver-minor + open-pull-requests-limit: 10 + labels: + - 'type: dependency-upgrade' + groups: + development-dependencies: + update-types: + - patch + patterns: + - com.gradle.enterprise + - com.github.spotbugs + - io.spring.* + - org.ajoberstar.grgit + - org.antora + - io.micrometer:micrometer-docs-generator + - com.willowtreeapps.assertk:assertk-jvm + - org.hibernate.validator:hibernate-validator + - org.apache.httpcomponents.client5:httpclient5 + - org.awaitility:awaitility + - org.xerial.snappy:snappy-java + - org.lz4:lz4-java + - com.github.luben:zstd-jni + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: saturday + labels: + - 'type: task' + groups: + development-dependencies: + patterns: + - '*' + + - package-ecosystem: github-actions + target-branch: 3.0.x + directory: / + schedule: + interval: weekly + day: saturday + labels: + - 'type: task' groups: development-dependencies: patterns: - - "*" + - '*' \ No newline at end of file From bcd3f7400f6d3691199437dcfd3951f6b2619847 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Feb 2024 17:12:51 +0000 Subject: [PATCH 393/737] Bump io.micrometer:micrometer-bom from 1.12.2 to 1.12.3 (#2623) Bumps [io.micrometer:micrometer-bom](https://github.com/micrometer-metrics/micrometer) from 1.12.2 to 1.12.3. - [Release notes](https://github.com/micrometer-metrics/micrometer/releases) - [Commits](https://github.com/micrometer-metrics/micrometer/compare/v1.12.2...v1.12.3) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 7bccbd80fa..97c513dd3e 100644 --- a/build.gradle +++ b/build.gradle @@ -61,7 +61,7 @@ ext { logbackVersion = '1.4.14' lz4Version = '1.8.0' micrometerDocsVersion = '1.0.2' - micrometerVersion = '1.12.2' + micrometerVersion = '1.12.3' micrometerTracingVersion = '1.2.2' mockitoVersion = '5.6.0' rabbitmqStreamVersion = '0.14.0' From 0593fde6a05315860beeed68e8b390e3353fd8ca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Feb 2024 17:14:33 +0000 Subject: [PATCH 394/737] Bump com.fasterxml.jackson:jackson-bom from 2.15.3 to 2.15.4 (#2619) Bumps [com.fasterxml.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) from 2.15.3 to 2.15.4. - [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-2.15.3...jackson-bom-2.15.4) --- updated-dependencies: - dependency-name: com.fasterxml.jackson:jackson-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 97c513dd3e..00a3e968dc 100644 --- a/build.gradle +++ b/build.gradle @@ -52,7 +52,7 @@ ext { commonsPoolVersion = '2.12.0' hamcrestVersion = '2.2' hibernateValidationVersion = '8.0.1.Final' - jacksonBomVersion = '2.15.3' + jacksonBomVersion = '2.15.4' jaywayJsonPathVersion = '2.8.0' junit4Version = '4.13.2' junitJupiterVersion = '5.10.2' From 2fb566d151bd771160504ec197b47eb2a8a471fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Feb 2024 17:15:05 +0000 Subject: [PATCH 395/737] Bump io.projectreactor:reactor-bom from 2023.0.2 to 2023.0.3 (#2624) Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2023.0.2 to 2023.0.3. - [Release notes](https://github.com/reactor/reactor/releases) - [Commits](https://github.com/reactor/reactor/compare/2023.0.2...2023.0.3) --- updated-dependencies: - dependency-name: io.projectreactor:reactor-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 00a3e968dc..26da2f1f5b 100644 --- a/build.gradle +++ b/build.gradle @@ -66,7 +66,7 @@ ext { mockitoVersion = '5.6.0' rabbitmqStreamVersion = '0.14.0' rabbitmqVersion = '5.19.0' - reactorVersion = '2023.0.2' + reactorVersion = '2023.0.3' snappyVersion = '1.1.10.5' springDataVersion = '2023.1.2' springRetryVersion = '2.0.5' From e1260555ed0dae5b05aeba234f2eeffa551d25fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Feb 2024 17:15:12 +0000 Subject: [PATCH 396/737] Bump org.springframework:spring-framework-bom from 6.1.3 to 6.1.4 (#2622) Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.1.3 to 6.1.4. - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.1.3...v6.1.4) --- updated-dependencies: - dependency-name: org.springframework:spring-framework-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 26da2f1f5b..39d8290254 100644 --- a/build.gradle +++ b/build.gradle @@ -70,7 +70,7 @@ ext { snappyVersion = '1.1.10.5' springDataVersion = '2023.1.2' springRetryVersion = '2.0.5' - springVersion = '6.1.3' + springVersion = '6.1.4' testcontainersVersion = '1.19.5' zstdJniVersion = '1.5.5-11' From c88fecb0146048f005d86abc94a704950781537b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Feb 2024 17:15:18 +0000 Subject: [PATCH 397/737] Bump org.springframework.data:spring-data-bom from 2023.1.2 to 2023.1.3 (#2621) Bumps [org.springframework.data:spring-data-bom](https://github.com/spring-projects/spring-data-bom) from 2023.1.2 to 2023.1.3. - [Release notes](https://github.com/spring-projects/spring-data-bom/releases) - [Commits](https://github.com/spring-projects/spring-data-bom/compare/2023.1.2...2023.1.3) --- updated-dependencies: - dependency-name: org.springframework.data:spring-data-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 39d8290254..39fad50b6d 100644 --- a/build.gradle +++ b/build.gradle @@ -68,7 +68,7 @@ ext { rabbitmqVersion = '5.19.0' reactorVersion = '2023.0.3' snappyVersion = '1.1.10.5' - springDataVersion = '2023.1.2' + springDataVersion = '2023.1.3' springRetryVersion = '2.0.5' springVersion = '6.1.4' testcontainersVersion = '1.19.5' From 0316192e4a480584a0489e4b30081dda7f6d2458 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Feb 2024 17:16:35 +0000 Subject: [PATCH 398/737] Bump io.micrometer:micrometer-tracing-bom from 1.2.2 to 1.2.3 (#2618) Bumps [io.micrometer:micrometer-tracing-bom](https://github.com/micrometer-metrics/tracing) from 1.2.2 to 1.2.3. - [Release notes](https://github.com/micrometer-metrics/tracing/releases) - [Commits](https://github.com/micrometer-metrics/tracing/compare/v1.2.2...v1.2.3) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-tracing-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 39fad50b6d..4cb485c77f 100644 --- a/build.gradle +++ b/build.gradle @@ -62,7 +62,7 @@ ext { lz4Version = '1.8.0' micrometerDocsVersion = '1.0.2' micrometerVersion = '1.12.3' - micrometerTracingVersion = '1.2.2' + micrometerTracingVersion = '1.2.3' mockitoVersion = '5.6.0' rabbitmqStreamVersion = '0.14.0' rabbitmqVersion = '5.19.0' From d0eb4fc74157fed3eb70f20cd16df34ea94e3306 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Fri, 16 Feb 2024 19:26:24 +0000 Subject: [PATCH 399/737] [artifactory-release] Release version 3.1.2 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 01eb10e1e3..045f58a008 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.1.2-SNAPSHOT +version=3.1.2 org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true From 78acbfcac4a52302acbedc5c986b3603d5c90bc2 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Fri, 16 Feb 2024 19:26:25 +0000 Subject: [PATCH 400/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 045f58a008..59c2ad5c2b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.1.2 +version=3.1.3-SNAPSHOT org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true From 1ab10e505202cb3f511b1958aa67d43a8a9795cb Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 21 Feb 2024 07:08:39 -0500 Subject: [PATCH 401/737] Tentative WF for pushing ot Maven Central --- .../workflows/manual-promote-to-central.yml | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/manual-promote-to-central.yml diff --git a/.github/workflows/manual-promote-to-central.yml b/.github/workflows/manual-promote-to-central.yml new file mode 100644 index 0000000000..6e00431884 --- /dev/null +++ b/.github/workflows/manual-promote-to-central.yml @@ -0,0 +1,28 @@ +name: Manual push to central + +on: + workflow_dispatch: + inputs: + buildName: + description: 'The Artifactory Build Name' + required: true + type: string + buildNumber: + description: 'The Artifactory Build Number' + required: true + type: string + +jobs: + push-to-manen-central: + uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-promote-central.yml@main + with: + buildName: ${{ inputs.buildName }} + buildNumber: ${{ inputs.buildNumber }} + secrets: + JF_ARTIFACTORY_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} + OSSRH_URL: ${{ secrets.OSSRH_URL }} + OSSRH_S01_TOKEN_USERNAME: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }} + OSSRH_S01_TOKEN_PASSWORD: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }} + OSSRH_STAGING_PROFILE_NAME: ${{ secrets.OSSRH_STAGING_PROFILE_NAME }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} \ No newline at end of file From 76844c2beb29edc4e6abc434bf4c2b9ea8f3bb75 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 21 Feb 2024 07:18:23 -0500 Subject: [PATCH 402/737] Revert "Tentative WF for pushing ot Maven Central" This reverts commit 1ab10e505202cb3f511b1958aa67d43a8a9795cb. --- .../workflows/manual-promote-to-central.yml | 28 ------------------- 1 file changed, 28 deletions(-) delete mode 100644 .github/workflows/manual-promote-to-central.yml diff --git a/.github/workflows/manual-promote-to-central.yml b/.github/workflows/manual-promote-to-central.yml deleted file mode 100644 index 6e00431884..0000000000 --- a/.github/workflows/manual-promote-to-central.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Manual push to central - -on: - workflow_dispatch: - inputs: - buildName: - description: 'The Artifactory Build Name' - required: true - type: string - buildNumber: - description: 'The Artifactory Build Number' - required: true - type: string - -jobs: - push-to-manen-central: - uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-promote-central.yml@main - with: - buildName: ${{ inputs.buildName }} - buildNumber: ${{ inputs.buildNumber }} - secrets: - JF_ARTIFACTORY_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} - OSSRH_URL: ${{ secrets.OSSRH_URL }} - OSSRH_S01_TOKEN_USERNAME: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }} - OSSRH_S01_TOKEN_PASSWORD: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }} - OSSRH_STAGING_PROFILE_NAME: ${{ secrets.OSSRH_STAGING_PROFILE_NAME }} - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} \ No newline at end of file From a6856f28a852c67109a3aca7372a2fc7df71cbb5 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 22 Feb 2024 09:55:05 -0500 Subject: [PATCH 403/737] Fix typo for `@SpringJUnitConfig` in the docs --- src/reference/antora/modules/ROOT/pages/testing.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/reference/antora/modules/ROOT/pages/testing.adoc b/src/reference/antora/modules/ROOT/pages/testing.adoc index 96e84abcd0..42571eb1a5 100644 --- a/src/reference/antora/modules/ROOT/pages/testing.adoc +++ b/src/reference/antora/modules/ROOT/pages/testing.adoc @@ -31,7 +31,7 @@ In addition, the beans associated with `@EnableRabbit` (to support `@RabbitListe .Junit5 example [source, java] ---- -@SpringJunitConfig +@SpringJUnitConfig @SpringRabbitTest public class MyRabbitTests { @@ -59,7 +59,7 @@ public class MyRabbitTests { } ---- -With JUnit4, replace `@SpringJunitConfig` with `@RunWith(SpringRunnner.class)`. +With JUnit4, replace `@SpringJUnitConfig` with `@RunWith(SpringRunnner.class)`. [[mockito-answer]] == Mockito `Answer` Implementations From b6c2efc4124343966b38c3e2c16cb980fffa5e52 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 22 Feb 2024 16:58:13 -0500 Subject: [PATCH 404/737] Use `spring-amqp.version` prop for verify-staged-artifacts **Auto-cherry-pick to `3.0.x`** --- .github/workflows/verify-staged-artifacts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verify-staged-artifacts.yml b/.github/workflows/verify-staged-artifacts.yml index d013118a08..2c678241a2 100644 --- a/.github/workflows/verify-staged-artifacts.yml +++ b/.github/workflows/verify-staged-artifacts.yml @@ -44,7 +44,7 @@ jobs: run: jf mvnc --repo-resolve-releases=libs-release-staging --repo-resolve-snapshots=snapshot - name: Verify samples against staged release - run: jf mvn verify -D"spring.amqp.version"=${{ inputs.releaseVersion }} -B -ntp + run: jf mvn verify -D"spring-amqp.version"=${{ inputs.releaseVersion }} -B -ntp - name: Capture Test Results if: failure() From 508f42678e73e58965033de75b72ddd81b4e8714 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 22 Feb 2024 17:18:27 -0500 Subject: [PATCH 405/737] Add `rabbitmq_stream` plugin to `verify-staged-artifacts` **Auto-cherry-pick to `3.0.x`** --- .github/workflows/verify-staged-artifacts.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/verify-staged-artifacts.yml b/.github/workflows/verify-staged-artifacts.yml index 2c678241a2..614c4d57ff 100644 --- a/.github/workflows/verify-staged-artifacts.yml +++ b/.github/workflows/verify-staged-artifacts.yml @@ -19,9 +19,13 @@ jobs: ports: - 5672:5672 - 15672:15672 + - 5552:5552 steps: + - name: Enable RabbitMQ Stream Plugin + run: docker exec ${{ job.services.rabbitmq.id }} rabbitmq-plugins enable rabbitmq_stream + - name: Checkout Samples Repo uses: actions/checkout@v4 with: From 97e68d3875774ff448a68cd8370f33920f19ac8b Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 22 Feb 2024 17:26:07 -0500 Subject: [PATCH 406/737] Try without Rabbit Streams --- .github/workflows/verify-staged-artifacts.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/verify-staged-artifacts.yml b/.github/workflows/verify-staged-artifacts.yml index 614c4d57ff..26ec293447 100644 --- a/.github/workflows/verify-staged-artifacts.yml +++ b/.github/workflows/verify-staged-artifacts.yml @@ -19,12 +19,12 @@ jobs: ports: - 5672:5672 - 15672:15672 - - 5552:5552 +# - 5552:5552 steps: - - name: Enable RabbitMQ Stream Plugin - run: docker exec ${{ job.services.rabbitmq.id }} rabbitmq-plugins enable rabbitmq_stream +# - name: Enable RabbitMQ Stream Plugin +# run: docker exec ${{ job.services.rabbitmq.id }} rabbitmq-plugins enable rabbitmq_stream - name: Checkout Samples Repo uses: actions/checkout@v4 From c0234b13714dc17d6dc3fef0a1952c0dede9121c Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 22 Feb 2024 17:52:16 -0500 Subject: [PATCH 407/737] More changes to verify-staged-artifacts * Enable support for Rabbit Streams * Use `mvn versions:set` since Spring AMQP Sample rely now on `project.version` for target `spring-amqp.version` --- .github/workflows/verify-staged-artifacts.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/verify-staged-artifacts.yml b/.github/workflows/verify-staged-artifacts.yml index 26ec293447..e34baf4c04 100644 --- a/.github/workflows/verify-staged-artifacts.yml +++ b/.github/workflows/verify-staged-artifacts.yml @@ -16,15 +16,17 @@ jobs: rabbitmq: image: rabbitmq:management + env: + RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS: -rabbitmq_stream advertised_host localhost ports: - 5672:5672 - 15672:15672 -# - 5552:5552 + - 5552:5552 steps: -# - name: Enable RabbitMQ Stream Plugin -# run: docker exec ${{ job.services.rabbitmq.id }} rabbitmq-plugins enable rabbitmq_stream + - name: Enable RabbitMQ Stream Plugin + run: docker exec ${{ job.services.rabbitmq.id }} rabbitmq-plugins enable rabbitmq_stream - name: Checkout Samples Repo uses: actions/checkout@v4 @@ -48,7 +50,9 @@ jobs: run: jf mvnc --repo-resolve-releases=libs-release-staging --repo-resolve-snapshots=snapshot - name: Verify samples against staged release - run: jf mvn verify -D"spring-amqp.version"=${{ inputs.releaseVersion }} -B -ntp + run: | + mvn versions:set -DnewVersion=${{ inputs.releaseVersion }} -DgenerateBackupPoms=false -DprocessAllModules=true -B -ntp + jf mvn verify -B -ntp - name: Capture Test Results if: failure() From f749fb1b30ceaac904c403a166748c0e17f44c02 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 22 Feb 2024 17:57:25 -0500 Subject: [PATCH 408/737] Add `rabbitmqctl wait` step into `verify-staged-artifacts` --- .github/workflows/verify-staged-artifacts.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/verify-staged-artifacts.yml b/.github/workflows/verify-staged-artifacts.yml index e34baf4c04..d49238c033 100644 --- a/.github/workflows/verify-staged-artifacts.yml +++ b/.github/workflows/verify-staged-artifacts.yml @@ -16,8 +16,6 @@ jobs: rabbitmq: image: rabbitmq:management - env: - RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS: -rabbitmq_stream advertised_host localhost ports: - 5672:5672 - 15672:15672 @@ -25,6 +23,9 @@ jobs: steps: + - name: Wait RabbitMQ is Up + run: docker exec ${{ job.services.rabbitmq.id }} rabbitmqctl wait --pid 1 --timeout 60 + - name: Enable RabbitMQ Stream Plugin run: docker exec ${{ job.services.rabbitmq.id }} rabbitmq-plugins enable rabbitmq_stream From 0e1cf33aea5fe9cee51fca053f70dc176c100189 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 22 Feb 2024 18:04:43 -0500 Subject: [PATCH 409/737] Add `RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS` env into `verify-staged-artifacts` --- .github/workflows/verify-staged-artifacts.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/verify-staged-artifacts.yml b/.github/workflows/verify-staged-artifacts.yml index d49238c033..82f036302e 100644 --- a/.github/workflows/verify-staged-artifacts.yml +++ b/.github/workflows/verify-staged-artifacts.yml @@ -16,6 +16,8 @@ jobs: rabbitmq: image: rabbitmq:management + env: + RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS: -rabbitmq_stream advertised_host localhost ports: - 5672:5672 - 15672:15672 From 83141edd987277bc8ad6c66ae0a57ff53958e8eb Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 22 Feb 2024 18:26:00 -0500 Subject: [PATCH 410/737] Use `namoshek/rabbitmq-github-action` --- .github/workflows/verify-staged-artifacts.yml | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/.github/workflows/verify-staged-artifacts.yml b/.github/workflows/verify-staged-artifacts.yml index 82f036302e..5f9b32bda5 100644 --- a/.github/workflows/verify-staged-artifacts.yml +++ b/.github/workflows/verify-staged-artifacts.yml @@ -12,24 +12,13 @@ jobs: verify-staged-with-samples: runs-on: ubuntu-latest - services: - - rabbitmq: - image: rabbitmq:management - env: - RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS: -rabbitmq_stream advertised_host localhost - ports: - - 5672:5672 - - 15672:15672 - - 5552:5552 - steps: - - name: Wait RabbitMQ is Up - run: docker exec ${{ job.services.rabbitmq.id }} rabbitmqctl wait --pid 1 --timeout 60 - - - name: Enable RabbitMQ Stream Plugin - run: docker exec ${{ job.services.rabbitmq.id }} rabbitmq-plugins enable rabbitmq_stream + - name: Start RabbitMQ + uses: namoshek/rabbitmq-github-action@v1 + with: + ports: '5672:5672 15672:15672 5552:5552' + plugins: rabbitmq_stream,rabbitmq_management - name: Checkout Samples Repo uses: actions/checkout@v4 From 90a80af635de334359dd4a90549b8e2c704e59b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 23 Feb 2024 22:47:45 -0500 Subject: [PATCH 411/737] Bump org.testcontainers:testcontainers-bom from 1.19.5 to 1.19.6 (#2635) Bumps [org.testcontainers:testcontainers-bom](https://github.com/testcontainers/testcontainers-java) from 1.19.5 to 1.19.6. - [Release notes](https://github.com/testcontainers/testcontainers-java/releases) - [Changelog](https://github.com/testcontainers/testcontainers-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/testcontainers/testcontainers-java/compare/1.19.5...1.19.6) --- updated-dependencies: - dependency-name: org.testcontainers:testcontainers-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4cb485c77f..b0c7485352 100644 --- a/build.gradle +++ b/build.gradle @@ -71,7 +71,7 @@ ext { springDataVersion = '2023.1.3' springRetryVersion = '2.0.5' springVersion = '6.1.4' - testcontainersVersion = '1.19.5' + testcontainersVersion = '1.19.6' zstdJniVersion = '1.5.5-11' javaProjects = subprojects - project(':spring-amqp-bom') From 08ead3b683520c3625ca5a77dc6e70ded4cdf951 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 28 Feb 2024 12:17:15 -0500 Subject: [PATCH 412/737] GH-2638: Fix for Kotlin `suspend` returns Fixes: #2638 The `AbstractAdaptableMessageListener.asyncSuccess()` assumes only `Mono` & `CompletableFuture` return types. With Kotlin `suspend` it is just plain `Object`. * Fix `AbstractAdaptableMessageListener.asyncSuccess()` to check if `returnType` is not simple `Object.class` before going deep for its generic actual argument. **Auto-cherry-pick to `3.0.x`** --- .../adapter/AbstractAdaptableMessageListener.java | 7 ++++--- .../rabbit/annotation/EnableRabbitKotlinTests.kt | 12 +++++------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java index 5f7215c49a..1f72fe8fa3 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2024 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. @@ -418,9 +418,10 @@ private void asyncSuccess(InvocationResult resultArg, Message request, Channel c } } else { - // We only get here with Mono and ListenableFuture which have exactly one type argument Type returnType = resultArg.getReturnType(); - if (returnType != null) { + // We only get here with Mono and CompletableFuture which have exactly one type argument + // Otherwise it might be Kotlin suspend function + if (returnType != null && !Object.class.getName().equals(returnType.getTypeName())) { Type[] actualTypeArguments = ((ParameterizedType) returnType).getActualTypeArguments(); if (actualTypeArguments.length > 0) { returnType = actualTypeArguments[0]; // NOSONAR diff --git a/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt b/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt index ea320a0567..397fe765e8 100644 --- a/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt +++ b/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt @@ -21,7 +21,6 @@ import assertk.assertions.isEqualTo import assertk.assertions.isTrue import org.junit.jupiter.api.Test import org.springframework.amqp.core.AcknowledgeMode -import org.springframework.amqp.core.MessageListener import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory import org.springframework.amqp.rabbit.connection.CachingConnectionFactory import org.springframework.amqp.rabbit.core.RabbitTemplate @@ -62,8 +61,9 @@ class EnableRabbitKotlinTests { @Test fun `send and wait for consume`(@Autowired registry: RabbitListenerEndpointRegistry) { val template = RabbitTemplate(this.config.cf()) - template.convertAndSend("kotlinQueue", "test") - assertThat(this.config.latch.await(10, TimeUnit.SECONDS)).isTrue() + template.setReplyTimeout(10_000) + val result = template.convertSendAndReceive("kotlinQueue", "test") + assertThat(result).isEqualTo("TEST") val listener = registry.getListenerContainer("single").messageListener assertThat(TestUtils.getPropertyValue(listener, "messagingMessageConverter.inferredArgumentType").toString()) .isEqualTo("class java.lang.String") @@ -82,11 +82,9 @@ class EnableRabbitKotlinTests { @EnableRabbit class Config { - val latch = CountDownLatch(1) - @RabbitListener(id = "single", queues = ["kotlinQueue"]) - suspend fun handle(@Suppress("UNUSED_PARAMETER") data: String) { - this.latch.countDown() + suspend fun handle(@Suppress("UNUSED_PARAMETER") data: String) : String? { + return data.uppercase() } @Bean From 9a8d741faa51030adcfba5bc745cb84554ec8cc2 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 28 Feb 2024 17:35:02 -0500 Subject: [PATCH 413/737] GH-2640: Fix leak for non-confirmed channel Fixes: #2640 Rabbit server was unstable for a while. Once restored, we were unable to publish new confirmed messages to it (the max number of channel on connection was reached and the existing channels were ignored). Essentially `PublisherCallbackChannel` instances ara waiting for acks on their confirms which never going to happen. Therefore, these channels are not closed and cache state is not reset. * Fix `CachingConnectionFactory.CachedChannelInvocationHandler.returnToCache()` to schedule `waitForConfirms()` in the separate thread. If `TimeoutException` happens, perform `physicalClose()` to avoid any possible memory leaks * Adjust `RabbitTemplatePublisherCallbacksIntegrationTests.testPublisherConfirmNotReceived()` to ensure that "unconfirmed" channel is closed and `CachingConnectionFactory` can produce a new channel **Auto-cherry-pick to `3.0.x`** --- .../connection/CachingConnectionFactory.java | 37 ++++++++++++++++--- ...atePublisherCallbacksIntegrationTests.java | 22 +++++++++-- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java index 7eff49b839..6cd745733e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -1303,13 +1303,38 @@ private void logicalClose(ChannelProxy proxy) throws IOException, TimeoutExcepti } private void returnToCache(ChannelProxy proxy) { - if (CachingConnectionFactory.this.active && this.publisherConfirms - && proxy instanceof PublisherCallbackChannel) { + if (CachingConnectionFactory.this.active + && this.publisherConfirms + && proxy instanceof PublisherCallbackChannel publisherCallbackChannel) { this.theConnection.channelsAwaitingAcks.put(this.target, proxy); - ((PublisherCallbackChannel) proxy) - .setAfterAckCallback(c -> - doReturnToCache(this.theConnection.channelsAwaitingAcks.remove(c))); + AtomicBoolean ackCallbackCalledImmediately = new AtomicBoolean(); + publisherCallbackChannel + .setAfterAckCallback(c -> { + ackCallbackCalledImmediately.set(true); + doReturnToCache(this.theConnection.channelsAwaitingAcks.remove(c)); + }); + + if (!ackCallbackCalledImmediately.get()) { + getChannelsExecutor() + .execute(() -> { + try { + publisherCallbackChannel.waitForConfirms(ASYNC_CLOSE_TIMEOUT); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + catch (TimeoutException ex) { + // The channel didn't handle confirms, so close it altogether to avoid + // memory leaks for pending confirms + try { + physicalClose(this.theConnection.channelsAwaitingAcks.remove(this.target)); + } + catch (@SuppressWarnings(UNUSED) Exception e) { + } + } + }); + } } else { doReturnToCache(proxy); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests.java index 5d68698991..1429b7f5e6 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests.java @@ -20,6 +20,7 @@ import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willAnswer; @@ -43,6 +44,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -326,6 +328,14 @@ public void testPublisherConfirmNotReceived() throws Exception { given(mockChannel.isOpen()).willReturn(true); given(mockChannel.getNextPublishSeqNo()).willReturn(1L); + CountDownLatch timeoutExceptionLatch = new CountDownLatch(1); + + given(mockChannel.waitForConfirms(anyLong())) + .willAnswer(invocation -> { + timeoutExceptionLatch.await(10, TimeUnit.SECONDS); + throw new TimeoutException(); + }); + given(mockConnectionFactory.newConnection(any(ExecutorService.class), anyString())).willReturn(mockConnection); given(mockConnection.isOpen()).willReturn(true); @@ -334,20 +344,26 @@ public void testPublisherConfirmNotReceived() throws Exception { .createChannel(); CachingConnectionFactory ccf = new CachingConnectionFactory(mockConnectionFactory); - ccf.setExecutor(mock(ExecutorService.class)); + ccf.setExecutor(Executors.newCachedThreadPool()); ccf.setPublisherConfirmType(ConfirmType.CORRELATED); + ccf.setChannelCacheSize(1); + ccf.setChannelCheckoutTimeout(10000); final RabbitTemplate template = new RabbitTemplate(ccf); final AtomicBoolean confirmed = new AtomicBoolean(); template.setConfirmCallback((correlationData, ack, cause) -> confirmed.set(true)); template.convertAndSend(ROUTE, (Object) "message", new CorrelationData("abc")); - Thread.sleep(5); + assertThat(template.getUnconfirmedCount()).isEqualTo(1); Collection unconfirmed = template.getUnconfirmed(-1); assertThat(template.getUnconfirmedCount()).isEqualTo(0); assertThat(unconfirmed).hasSize(1); assertThat(unconfirmed.iterator().next().getId()).isEqualTo("abc"); assertThat(confirmed.get()).isFalse(); + + timeoutExceptionLatch.countDown(); + + assertThat(ccf.createConnection().createChannel(false)).isNotNull(); } @Test @@ -563,7 +579,7 @@ public void testPublisherConfirmMultipleWithTwoListeners() throws Exception { * time as adding a new pending ack to the map. Test verifies we don't * get a {@link ConcurrentModificationException}. */ - @SuppressWarnings({ "rawtypes", "unchecked" }) + @SuppressWarnings({"rawtypes", "unchecked"}) @Test public void testConcurrentConfirms() throws Exception { ConnectionFactory mockConnectionFactory = mock(ConnectionFactory.class); From 8c73342fd95e6dc513b3bb0b49e8d4effdd32d02 Mon Sep 17 00:00:00 2001 From: Maksim Sasnouski Date: Thu, 29 Feb 2024 19:39:46 +0300 Subject: [PATCH 414/737] Upgrade com.gradle.enterprise plugin to 3.16.2 --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index 23d263506e..558e054457 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,5 @@ plugins { - id 'com.gradle.enterprise' version '3.14.1' + id 'com.gradle.enterprise' version '3.16.2' id 'io.spring.ge.conventions' version '0.0.15' } From 0cac07b1cb2afa0ab93ba7b622c49312eb091a3c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Mar 2024 02:03:40 +0000 Subject: [PATCH 415/737] Bump the development-dependencies group with 1 update (#2643) Bumps the development-dependencies group with 1 update: com.github.spotbugs. Updates `com.github.spotbugs` from 6.0.7 to 6.0.8 --- updated-dependencies: - dependency-name: com.github.spotbugs dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b0c7485352..a26165479a 100644 --- a/build.gradle +++ b/build.gradle @@ -24,7 +24,7 @@ plugins { id 'io.spring.dependency-management' version '1.1.4' apply false id 'org.antora' version '1.0.0' id 'io.spring.antora.generate-antora-yml' version '0.0.1' - id 'com.github.spotbugs' version '6.0.7' + id 'com.github.spotbugs' version '6.0.8' } description = 'Spring AMQP' From c960f338596ba20600bbe5971c94bfc6a9fc2acd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 9 Mar 2024 02:22:44 +0000 Subject: [PATCH 416/737] Bump kotlinVersion from 1.9.22 to 1.9.23 (#2648) Bumps `kotlinVersion` from 1.9.22 to 1.9.23. Updates `org.jetbrains.kotlin:kotlin-gradle-plugin` from 1.9.22 to 1.9.23 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/v1.9.23/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.9.22...v1.9.23) Updates `org.jetbrains.kotlin:kotlin-allopen` from 1.9.22 to 1.9.23 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/v1.9.23/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.9.22...v1.9.23) --- updated-dependencies: - dependency-name: org.jetbrains.kotlin:kotlin-gradle-plugin dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.jetbrains.kotlin:kotlin-allopen dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a26165479a..9dfe1ffc7b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlinVersion = '1.9.22' + ext.kotlinVersion = '1.9.23' ext.isCI = System.getenv('GITHUB_ACTION') repositories { mavenCentral() From 8bc8b5acc1871c80cc51183490bd8d6223212b82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 9 Mar 2024 02:22:51 +0000 Subject: [PATCH 417/737] Bump org.testcontainers:testcontainers-bom from 1.19.6 to 1.19.7 (#2649) Bumps [org.testcontainers:testcontainers-bom](https://github.com/testcontainers/testcontainers-java) from 1.19.6 to 1.19.7. - [Release notes](https://github.com/testcontainers/testcontainers-java/releases) - [Changelog](https://github.com/testcontainers/testcontainers-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/testcontainers/testcontainers-java/compare/1.19.6...1.19.7) --- updated-dependencies: - dependency-name: org.testcontainers:testcontainers-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 9dfe1ffc7b..9df212ede9 100644 --- a/build.gradle +++ b/build.gradle @@ -71,7 +71,7 @@ ext { springDataVersion = '2023.1.3' springRetryVersion = '2.0.5' springVersion = '6.1.4' - testcontainersVersion = '1.19.6' + testcontainersVersion = '1.19.7' zstdJniVersion = '1.5.5-11' javaProjects = subprojects - project(':spring-amqp-bom') From d135246d713be2fdd063440ea39be005d85388b3 Mon Sep 17 00:00:00 2001 From: Salk Lee Date: Wed, 13 Mar 2024 22:21:53 +0800 Subject: [PATCH 418/737] Add BackOff support into `ConnectionFactory` Make a `connection.createChannel()` as retryable operation based on the provided `BackOff` --- .../connection/AbstractConnectionFactory.java | 22 +++++++++-- .../rabbit/connection/SimpleConnection.java | 38 +++++++++++++++++-- .../AbstractConnectionFactoryTests.java | 25 +++++++++++- .../modules/ROOT/pages/amqp/connections.adoc | 3 ++ .../antora/modules/ROOT/pages/whats-new.adoc | 8 ++++ 5 files changed, 89 insertions(+), 7 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java index 2acc7e2546..33cb11dccf 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -53,6 +53,7 @@ import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; +import org.springframework.util.backoff.BackOff; import com.rabbitmq.client.Address; import com.rabbitmq.client.AddressResolver; @@ -71,6 +72,7 @@ * @author Artem Bilan * @author Will Droste * @author Christian Tzolov + * @author Salk Lee * */ public abstract class AbstractConnectionFactory implements ConnectionFactory, DisposableBean, BeanNameAware, @@ -162,6 +164,8 @@ public void handleRecovery(Recoverable recoverable) { private volatile boolean contextStopped; + @Nullable + private BackOff connectionCreatingBackOff; /** * Create a new AbstractConnectionFactory for the given target ConnectionFactory, with no publisher connection * factory. @@ -556,6 +560,18 @@ public boolean hasPublisherConnectionFactory() { return this.publisherConnectionFactory != null; } + /** + * Set the backoff strategy for creating connections. This enhancement supports custom + * retry policies within the connection module, particularly useful when the maximum + * channel limit is reached. The {@link SimpleConnection#createChannel(boolean)} method + * utilizes this backoff strategy to gracefully handle such limit exceptions. + * @param backOff the backoff strategy to be applied during connection creation + * @since 3.1.3 + */ + public void setConnectionCreatingBackOff(@Nullable BackOff backOff) { + this.connectionCreatingBackOff = backOff; + } + @Override public ConnectionFactory getPublisherConnectionFactory() { return this.publisherConnectionFactory; @@ -566,8 +582,8 @@ protected final Connection createBareConnection() { String connectionName = this.connectionNameStrategy.obtainNewConnectionName(this); com.rabbitmq.client.Connection rabbitConnection = connect(connectionName); - - Connection connection = new SimpleConnection(rabbitConnection, this.closeTimeout); + Connection connection = new SimpleConnection(rabbitConnection, this.closeTimeout, + this.connectionCreatingBackOff == null ? null : this.connectionCreatingBackOff.start()); if (rabbitConnection instanceof AutorecoveringConnection auto) { auto.addRecoveryListener(new RecoveryListener() { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleConnection.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleConnection.java index 4c14f1d02f..9815f2c8cf 100755 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleConnection.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -19,9 +19,13 @@ import java.io.IOException; import java.net.InetAddress; +import javax.annotation.Nullable; + import org.springframework.amqp.AmqpResourceNotAvailableException; +import org.springframework.amqp.AmqpTimeoutException; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.util.ObjectUtils; +import org.springframework.util.backoff.BackOffExecution; import com.rabbitmq.client.AlreadyClosedException; import com.rabbitmq.client.BlockedListener; @@ -35,6 +39,7 @@ * @author Dave Syer * @author Gary Russell * @author Artem Bilan + * @author Salk Lee * * @since 1.0 */ @@ -46,16 +51,39 @@ public class SimpleConnection implements Connection, NetworkConnection { private volatile boolean explicitlyClosed; - public SimpleConnection(com.rabbitmq.client.Connection delegate, - int closeTimeout) { + @Nullable + private final BackOffExecution backOffExecution; + + public SimpleConnection(com.rabbitmq.client.Connection delegate, int closeTimeout) { + this(delegate, closeTimeout, null); + } + + /** + * Construct an instance with the {@link org.springframework.util.backoff.BackOffExecution} arguments. + * @param delegate delegate connection + * @param closeTimeout the time of physical close time out + * @param backOffExecution backOffExecution is nullable + * @since 3.1.3 + */ + public SimpleConnection(com.rabbitmq.client.Connection delegate, int closeTimeout, + @Nullable BackOffExecution backOffExecution) { this.delegate = delegate; this.closeTimeout = closeTimeout; + this.backOffExecution = backOffExecution; } @Override public Channel createChannel(boolean transactional) { try { Channel channel = this.delegate.createChannel(); + while (channel == null && this.backOffExecution != null) { + long interval = this.backOffExecution.nextBackOff(); + if (interval == BackOffExecution.STOP) { + break; + } + Thread.sleep(interval); + channel = this.delegate.createChannel(); + } if (channel == null) { throw new AmqpResourceNotAvailableException("The channelMax limit is reached. Try later."); } @@ -65,6 +93,10 @@ public Channel createChannel(boolean transactional) { } return channel; } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new AmqpTimeoutException("Interrupted while creating a new channel", e); + } catch (IOException e) { throw RabbitExceptionTranslator.convertRabbitAccessException(e); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactoryTests.java index 60e2591b72..58546447c8 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2023 the original author or authors. + * Copyright 2010-2024 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. @@ -17,6 +17,7 @@ package org.springframework.amqp.rabbit.connection; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; @@ -39,10 +40,13 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +import org.springframework.amqp.AmqpResourceNotAvailableException; import org.springframework.amqp.rabbit.connection.AbstractConnectionFactory.AddressShuffleMode; import org.springframework.amqp.utils.test.TestUtils; import org.springframework.beans.DirectFieldAccessor; import org.springframework.scheduling.concurrent.CustomizableThreadFactory; +import org.springframework.util.StopWatch; +import org.springframework.util.backoff.FixedBackOff; import com.rabbitmq.client.Channel; import com.rabbitmq.client.ConnectionFactory; @@ -52,6 +56,7 @@ * @author Gary Russell * @author Dmitry Dbrazhnikov * @author Artem Bilan + * @author Salk Lee */ public abstract class AbstractConnectionFactoryTests { @@ -212,4 +217,22 @@ public void testCreatesConnectionWithGivenFactory() { assertThat(mockConnectionFactory.getThreadFactory()).isEqualTo(connectionThreadFactory); } + @Test + public void testConnectionCreatingBackOff() throws Exception { + int maxAttempts = 2; + long interval = 100L; + com.rabbitmq.client.Connection mockConnection = mock(com.rabbitmq.client.Connection.class); + given(mockConnection.createChannel()).willReturn(null); + SimpleConnection simpleConnection = new SimpleConnection(mockConnection, 5, + new FixedBackOff(interval, maxAttempts).start()); + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + assertThatExceptionOfType(AmqpResourceNotAvailableException.class).isThrownBy(() -> { + simpleConnection.createChannel(false); + }); + stopWatch.stop(); + assertThat(stopWatch.getTotalTimeMillis()).isGreaterThanOrEqualTo(maxAttempts * interval); + verify(mockConnection, times(maxAttempts + 1)).createChannel(); + } + } diff --git a/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc b/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc index 44abca5502..42a4b0a199 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc @@ -28,6 +28,9 @@ Simple publisher confirmations are supported by all three factories. When configuring a `RabbitTemplate` to use a xref:amqp/template.adoc#separate-connection[separate connection], you can now, starting with version 2.3.2, configure the publishing connection factory to be a different type. By default, the publishing factory is the same type and any properties set on the main factory are also propagated to the publishing factory. +Starting with version 3.1, the `AbstractConnectionFactory` includes the `connectionCreatingBackOff` property, which supports a backoff policy in the connection module. +Currently, there is support in the behavior of `createChannel()` to handle exceptions that occur when the `channelMax` limit is reached, implementing a backoff strategy based on attempts and intervals. + [[pooledchannelconnectionfactory]] === `PooledChannelConnectionFactory` diff --git a/src/reference/antora/modules/ROOT/pages/whats-new.adoc b/src/reference/antora/modules/ROOT/pages/whats-new.adoc index ff9eb62ad4..4c933d4213 100644 --- a/src/reference/antora/modules/ROOT/pages/whats-new.adoc +++ b/src/reference/antora/modules/ROOT/pages/whats-new.adoc @@ -18,3 +18,11 @@ It remains possible to configure your own logging behavior by setting the `exclu In addition, the `SimpleMessageListenerContainer` consumer restart after such an exception is now logged at DEBUG level by default (previously INFO). A new method `logRestart()` has been added to the `ConditionalExceptionLogger` to allow this to be changed. See xref:amqp/receiving-messages/consumer-events.adoc[Consumer Events] and <> for more information. + +[[x31-conn-backoff]] +=== Connections Enhancement + +Connection Factory supported backoff policy when creating connection channel. +See xref:amqp/connections.adoc[Choosing a Connection Factory] for more information. + + From 0049ce74b989955a2906124b6aaf032ff5bf0be4 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 13 Mar 2024 14:49:56 -0400 Subject: [PATCH 419/737] GH-2652: Make RabbitListenerErrorHandler aware of Channel Fixes: #2652 Whenever a `MessageConversionException` occurs, the raw Spring message returned in `RabbitListenerErrorHandler#handleError` is null. Channel information is being stored inside of that raw message, therefore it is not possible to manually nack just failed message * Introduce a new `handleError(Message, Channel, Message, ListenerExecutionFailedException)` contract to make a `Channel` access independent of the `Message` result * Deprecate existing method with the plan to remove in the next version (see #2654) --- .../MessagingMessageListenerAdapter.java | 5 ++-- .../api/RabbitListenerErrorHandler.java | 26 ++++++++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java index 60a67e3075..6e3579f8c3 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -182,10 +182,11 @@ private void handleException(org.springframework.amqp.core.Message amqpMessage, Message messageWithChannel = null; if (message != null) { messageWithChannel = MessageBuilder.fromMessage(message) + // TODO won't be necessary starting with version 3.2 .setHeader(AmqpHeaders.CHANNEL, channel) .build(); } - Object errorResult = this.errorHandler.handleError(amqpMessage, messageWithChannel, e); + Object errorResult = this.errorHandler.handleError(amqpMessage, channel, messageWithChannel, e); if (errorResult != null) { Object payload = message == null ? null : message.getPayload(); InvocationResult invResult = payload == null diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/RabbitListenerErrorHandler.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/RabbitListenerErrorHandler.java index 745e351d31..3a01921f2e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/RabbitListenerErrorHandler.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/RabbitListenerErrorHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 the original author or authors. + * Copyright 2016-2024 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. @@ -20,6 +20,7 @@ import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; import org.springframework.lang.Nullable; +import com.rabbitmq.client.Channel; /** * An error handler which is called when a {code @RabbitListener} method * throws an exception. This is invoked higher up the stack than the @@ -41,8 +42,31 @@ public interface RabbitListenerErrorHandler { * {@link ListenerExecutionFailedException}. * @return the return value to be sent to the sender. * @throws Exception an exception which may be the original or different. + * @deprecated in favor of + * {@link #handleError(Message, Channel, org.springframework.messaging.Message, ListenerExecutionFailedException)} */ + @Deprecated(forRemoval = true, since = "3.1.3") Object handleError(Message amqpMessage, @Nullable org.springframework.messaging.Message message, ListenerExecutionFailedException exception) throws Exception; // NOSONAR + /** + * Handle the error. If an exception is not thrown, the return value is returned to + * the sender using normal {@code replyTo/@SendTo} semantics. + * @param amqpMessage the raw message received. + * @param channel AMQP channel for manual acks. + * @param message the converted spring-messaging message (if available). + * @param exception the exception the listener threw, wrapped in a + * {@link ListenerExecutionFailedException}. + * @return the return value to be sent to the sender. + * @throws Exception an exception which may be the original or different. + * @since 3.1.3 + */ + @SuppressWarnings("deprecation") + default Object handleError(Message amqpMessage, Channel channel, + @Nullable org.springframework.messaging.Message message, + ListenerExecutionFailedException exception) throws Exception { // NOSONAR + + return handleError(amqpMessage, message, exception); + } + } From 22d22b97fb99fba1bb3ce50bd0683333b396771f Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 13 Mar 2024 15:01:00 -0400 Subject: [PATCH 420/737] CCF.CChInvH: physicalClose() on ShutdownSignalException The `publisherCallbackChannel.waitForConfirms()` may fail with a `ShutdownSignalException` indicating that channel is closed on the broker. So, treat it as a `TimeoutException` and proceed with a `physicalClose()` logic **Auto-cherry-pick to `3.0.x`** Related to #2640 --- .../amqp/rabbit/connection/CachingConnectionFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java index 6cd745733e..57fe722693 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java @@ -1324,7 +1324,7 @@ private void returnToCache(ChannelProxy proxy) { catch (InterruptedException ex) { Thread.currentThread().interrupt(); } - catch (TimeoutException ex) { + catch (ShutdownSignalException | TimeoutException ex) { // The channel didn't handle confirms, so close it altogether to avoid // memory leaks for pending confirms try { From 2b11825d52eb776fc4b43496baeabee18495c769 Mon Sep 17 00:00:00 2001 From: Tim Tran <49887921+tacascer@users.noreply.github.com> Date: Fri, 15 Mar 2024 10:18:23 -0400 Subject: [PATCH 421/737] GH-2647: Warn in docs for batch listeners and observation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: #2647 * docs: fix link for micrometer.adocˆ * docs: add note on batch listeners with observation --- .../amqp/receiving-messages/micrometer-observation.adoc | 1 + .../ROOT/pages/amqp/receiving-messages/micrometer.adoc | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer-observation.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer-observation.adoc index fadad7cccc..d4e53cfcf8 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer-observation.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer-observation.adoc @@ -17,3 +17,4 @@ You can either subclass `DefaultRabbitTemplateObservationConvention` or `Default See xref:appendix/micrometer.adoc[Micrometer Observation Documentation] for more details. +WARNING: Due to ambiguity in how traces should be handled in a batch, observations are *NOT* created for xref:amqp/receiving-messages/batch.adoc[Batch Listener Containers]. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer.adoc index 3715ee6988..efae24ebfb 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer.adoc @@ -1,7 +1,10 @@ [[micrometer]] -= Monitoring Listener Performance += Micrometer Integration :page-section-summary-toc: 1 +NOTE: This section documents the integration with https://docs.micrometer.io/micrometer/reference/[Micrometer]. +For integration with Micrometer Observation, see xref:amqp/receiving-messages/micrometer-observation.adoc[Micrometer Observation]. + Starting with version 2.2, the listener containers will automatically create and update Micrometer `Timer` s for the listener, if `Micrometer` is detected on the class path, and a single `MeterRegistry` is present in the application context (or exactly one is annotated `@Primary`, such as when using Spring Boot). The timers can be disabled by setting the container property `micrometerEnabled` to `false`. @@ -16,6 +19,3 @@ The timers are named `spring.rabbitmq.listener` and have the following tags: * `exception` : `none` or `ListenerExecutionFailedException` You can add additional tags using the `micrometerTags` container property. - -Also see xref:stream.adoc#stream-micrometer-observation[Micrometer Observation]. - From 16b11e847306371a2d94d85bd258ec8477e0f101 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 16 Mar 2024 02:13:05 +0000 Subject: [PATCH 422/737] Bump io.micrometer:micrometer-bom from 1.12.3 to 1.12.4 (#2658) Bumps [io.micrometer:micrometer-bom](https://github.com/micrometer-metrics/micrometer) from 1.12.3 to 1.12.4. - [Release notes](https://github.com/micrometer-metrics/micrometer/releases) - [Commits](https://github.com/micrometer-metrics/micrometer/compare/v1.12.3...v1.12.4) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 9df212ede9..0dc61569f4 100644 --- a/build.gradle +++ b/build.gradle @@ -61,7 +61,7 @@ ext { logbackVersion = '1.4.14' lz4Version = '1.8.0' micrometerDocsVersion = '1.0.2' - micrometerVersion = '1.12.3' + micrometerVersion = '1.12.4' micrometerTracingVersion = '1.2.3' mockitoVersion = '5.6.0' rabbitmqStreamVersion = '0.14.0' From 3b619330c1ab474b63e5d5a7d756accaeb3e81b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 16 Mar 2024 02:13:23 +0000 Subject: [PATCH 423/737] Bump the development-dependencies group with 1 update (#2657) Bumps the development-dependencies group with 1 update: [org.awaitility:awaitility](https://github.com/awaitility/awaitility). Updates `org.awaitility:awaitility` from 4.2.0 to 4.2.1 - [Changelog](https://github.com/awaitility/awaitility/blob/master/changelog.txt) - [Commits](https://github.com/awaitility/awaitility/compare/awaitility-4.2.0...awaitility-4.2.1) --- updated-dependencies: - dependency-name: org.awaitility:awaitility dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0dc61569f4..cef98669e1 100644 --- a/build.gradle +++ b/build.gradle @@ -46,7 +46,7 @@ ext { assertjVersion = '3.24.2' assertkVersion = '0.27.0' - awaitilityVersion = '4.2.0' + awaitilityVersion = '4.2.1' commonsCompressVersion = '1.20' commonsHttpClientVersion = '5.2.3' commonsPoolVersion = '2.12.0' From 0689db2525156917c89ce61d930c365a5ccbf318 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 16 Mar 2024 02:14:40 +0000 Subject: [PATCH 424/737] Bump io.projectreactor:reactor-bom from 2023.0.3 to 2023.0.4 (#2659) Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2023.0.3 to 2023.0.4. - [Release notes](https://github.com/reactor/reactor/releases) - [Commits](https://github.com/reactor/reactor/compare/2023.0.3...2023.0.4) --- updated-dependencies: - dependency-name: io.projectreactor:reactor-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index cef98669e1..eff690290f 100644 --- a/build.gradle +++ b/build.gradle @@ -66,7 +66,7 @@ ext { mockitoVersion = '5.6.0' rabbitmqStreamVersion = '0.14.0' rabbitmqVersion = '5.19.0' - reactorVersion = '2023.0.3' + reactorVersion = '2023.0.4' snappyVersion = '1.1.10.5' springDataVersion = '2023.1.3' springRetryVersion = '2.0.5' From 76b27635f8e12f351bb701e37fbdca99c54a93c9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 16 Mar 2024 02:14:53 +0000 Subject: [PATCH 425/737] Bump org.springframework.data:spring-data-bom from 2023.1.3 to 2023.1.4 (#2662) Bumps [org.springframework.data:spring-data-bom](https://github.com/spring-projects/spring-data-bom) from 2023.1.3 to 2023.1.4. - [Release notes](https://github.com/spring-projects/spring-data-bom/releases) - [Commits](https://github.com/spring-projects/spring-data-bom/compare/2023.1.3...2023.1.4) --- updated-dependencies: - dependency-name: org.springframework.data:spring-data-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index eff690290f..cffa426988 100644 --- a/build.gradle +++ b/build.gradle @@ -68,7 +68,7 @@ ext { rabbitmqVersion = '5.19.0' reactorVersion = '2023.0.4' snappyVersion = '1.1.10.5' - springDataVersion = '2023.1.3' + springDataVersion = '2023.1.4' springRetryVersion = '2.0.5' springVersion = '6.1.4' testcontainersVersion = '1.19.7' From 35fa64e068f052ce8e1e640ab3de0327469caf89 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 16 Mar 2024 02:15:00 +0000 Subject: [PATCH 426/737] Bump org.springframework:spring-framework-bom from 6.1.4 to 6.1.5 (#2660) Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.1.4 to 6.1.5. - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.1.4...v6.1.5) --- updated-dependencies: - dependency-name: org.springframework:spring-framework-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index cffa426988..bb0f5e30ad 100644 --- a/build.gradle +++ b/build.gradle @@ -70,7 +70,7 @@ ext { snappyVersion = '1.1.10.5' springDataVersion = '2023.1.4' springRetryVersion = '2.0.5' - springVersion = '6.1.4' + springVersion = '6.1.5' testcontainersVersion = '1.19.7' zstdJniVersion = '1.5.5-11' From 4c4b6b6d6e3e59b3ed79b73861e8a11b331bb24a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 16 Mar 2024 02:16:50 +0000 Subject: [PATCH 427/737] Bump io.micrometer:micrometer-tracing-bom from 1.2.3 to 1.2.4 (#2661) Bumps [io.micrometer:micrometer-tracing-bom](https://github.com/micrometer-metrics/tracing) from 1.2.3 to 1.2.4. - [Release notes](https://github.com/micrometer-metrics/tracing/releases) - [Commits](https://github.com/micrometer-metrics/tracing/compare/v1.2.3...v1.2.4) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-tracing-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index bb0f5e30ad..bd93d6b9ca 100644 --- a/build.gradle +++ b/build.gradle @@ -62,7 +62,7 @@ ext { lz4Version = '1.8.0' micrometerDocsVersion = '1.0.2' micrometerVersion = '1.12.4' - micrometerTracingVersion = '1.2.3' + micrometerTracingVersion = '1.2.4' mockitoVersion = '5.6.0' rabbitmqStreamVersion = '0.14.0' rabbitmqVersion = '5.19.0' From b19fa8c321d44f7f3bb7340ef332438a2fff332f Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 18 Mar 2024 11:09:10 -0400 Subject: [PATCH 428/737] GH-2653: Fix deadlock in the DirectMessageListenerContainer Fixes: #2653 When not enough channel in the cache, the `DirectMessageListenerContainer.consume()` returns null and `adjustConsumers()` goes into an infinite loop, since already active consumer does not release its channel. * Fix `DirectMessageListenerContainer.consume()` to re-throw an `AmqpTimeoutException` which is thrown when no available channels in the cache * Catch `AmqpTimeoutException` in the `DirectReplyToMessageListenerContainer.getChannelHolder()` and reset `this.consumerCount--` to allow to try existing consumer until it is available, e.g. when this one receives a reply or times out. * Change `DirectReplyToMessageListenerContainer.consumerCount` to `AtomicInteger` --- .../DirectMessageListenerContainer.java | 6 +- ...DirectReplyToMessageListenerContainer.java | 66 +++++++++++-------- .../amqp/rabbit/AsyncRabbitTemplateTests.java | 40 +++++++++-- .../EnableRabbitBatchIntegrationTests.java | 8 ++- 4 files changed, 85 insertions(+), 35 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java index 7cdc31697a..b90c8c0f14 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 the original author or authors. + * Copyright 2016-2024 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. @@ -48,6 +48,7 @@ import org.springframework.amqp.AmqpConnectException; import org.springframework.amqp.AmqpException; import org.springframework.amqp.AmqpIOException; +import org.springframework.amqp.AmqpTimeoutException; import org.springframework.amqp.ImmediateAcknowledgeAmqpException; import org.springframework.amqp.core.AmqpAdmin; import org.springframework.amqp.core.Message; @@ -802,6 +803,9 @@ private SimpleConsumer consume(String queue, int index, Connection connection) { catch (AmqpApplicationContextClosedException e) { throw new AmqpConnectException(e); } + catch (AmqpTimeoutException timeoutException) { + throw timeoutException; + } catch (Exception e) { RabbitUtils.closeChannel(channel); RabbitUtils.closeConnection(connection); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainer.java index b639e11e33..6a777b0865 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 the original author or authors. + * Copyright 2016-2024 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. @@ -18,7 +18,9 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; +import org.springframework.amqp.AmqpTimeoutException; import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.Address; import org.springframework.amqp.core.MessageListener; @@ -47,7 +49,7 @@ public class DirectReplyToMessageListenerContainer extends DirectMessageListener private final ConcurrentMap whenUsed = new ConcurrentHashMap<>(); - private int consumerCount; + private final AtomicInteger consumerCount = new AtomicInteger(); public DirectReplyToMessageListenerContainer(ConnectionFactory connectionFactory) { super(connectionFactory); @@ -109,7 +111,7 @@ public void setMessageListener(MessageListener messageListener) { @Override protected void doStart() { if (!isRunning()) { - this.consumerCount = 0; + this.consumerCount.set(0); super.setConsumersPerQueue(0); super.doStart(); } @@ -118,23 +120,24 @@ protected void doStart() { @Override protected void processMonitorTask() { long now = System.currentTimeMillis(); + long reduce; this.consumersLock.lock(); try { - long reduce = this.consumers.stream() - .filter(c -> this.whenUsed.containsKey(c) && !this.inUseConsumerChannels.containsValue(c) - && this.whenUsed.get(c) < now - getIdleEventInterval()) - .count(); - if (reduce > 0) { - if (logger.isDebugEnabled()) { - logger.debug("Reducing idle consumes by " + reduce); - } - this.consumerCount = (int) Math.max(0, this.consumerCount - reduce); - super.setConsumersPerQueue(this.consumerCount); - } + reduce = this.consumers.stream() + .filter(c -> this.whenUsed.containsKey(c) && !this.inUseConsumerChannels.containsValue(c) + && this.whenUsed.get(c) < now - getIdleEventInterval()) + .count(); } finally { this.consumersLock.unlock(); } + if (reduce > 0) { + if (logger.isDebugEnabled()) { + logger.debug("Reducing idle consumes by " + reduce); + } + super.setConsumersPerQueue( + this.consumerCount.updateAndGet((current) -> (int) Math.max(0, current - reduce))); + } } @Override @@ -159,13 +162,13 @@ protected void consumerRemoved(SimpleConsumer consumer) { * @return the channel holder. */ public ChannelHolder getChannelHolder() { - this.consumersLock.lock(); - try { - ChannelHolder channelHolder = null; - while (channelHolder == null) { - if (!isRunning()) { - throw new IllegalStateException("Direct reply-to container is not running"); - } + ChannelHolder channelHolder = null; + while (channelHolder == null) { + if (!isRunning()) { + throw new IllegalStateException("Direct reply-to container is not running"); + } + this.consumersLock.lock(); + try { for (SimpleConsumer consumer : this.consumers) { Channel candidate = consumer.getChannel(); if (candidate.isOpen() && this.inUseConsumerChannels.putIfAbsent(candidate, consumer) == null) { @@ -175,16 +178,23 @@ public ChannelHolder getChannelHolder() { break; } } - if (channelHolder == null) { - this.consumerCount++; - super.setConsumersPerQueue(this.consumerCount); + } + finally { + this.consumersLock.unlock(); + } + if (channelHolder == null) { + try { + super.setConsumersPerQueue(this.consumerCount.incrementAndGet()); + } + catch (AmqpTimeoutException timeoutException) { + // Possibly No available channels in the cache, so come back to consumers + // iteration until existing is available + this.consumerCount.decrementAndGet(); } } - return channelHolder; - } - finally { - this.consumersLock.unlock(); } + return channelHolder; + } /** diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java index eccba25c8f..4d9a1bca81 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java @@ -17,6 +17,7 @@ package org.springframework.amqp.rabbit; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.fail; import static org.awaitility.Awaitility.await; import static org.mockito.Mockito.mock; @@ -29,6 +30,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; @@ -111,7 +113,8 @@ public void testConvert1ArgDirect() throws Exception { waitForZeroInUseConsumers(); assertThat(TestUtils .getPropertyValue(this.asyncDirectTemplate, "directReplyToContainer.consumerCount", - Integer.class)).isEqualTo(2); + AtomicInteger.class).get()) + .isEqualTo(2); final String missingQueue = UUID.randomUUID().toString(); this.asyncDirectTemplate.convertSendAndReceive("", missingQueue, "foo"); // send to nowhere this.asyncDirectTemplate.stop(); // should clear the inUse channel map @@ -168,18 +171,20 @@ public void testMessage1ArgDirect() throws Exception { waitForZeroInUseConsumers(); assertThat(TestUtils .getPropertyValue(this.asyncDirectTemplate, "directReplyToContainer.consumerCount", - Integer.class)).isEqualTo(2); + AtomicInteger.class).get()) + .isEqualTo(2); this.asyncDirectTemplate.stop(); this.asyncDirectTemplate.start(); assertThat(TestUtils .getPropertyValue(this.asyncDirectTemplate, "directReplyToContainer.consumerCount", - Integer.class)).isEqualTo(0); + AtomicInteger.class).get()) + .isEqualTo(0); } - private void waitForZeroInUseConsumers() throws InterruptedException { + private void waitForZeroInUseConsumers() { Map inUseConsumers = TestUtils .getPropertyValue(this.asyncDirectTemplate, "directReplyToContainer.inUseConsumerChannels", Map.class); - await().until(() -> inUseConsumers.size() == 0); + await().until(inUseConsumers::isEmpty); } @Test @@ -422,6 +427,31 @@ void ctorCoverage() { .isEqualTo("rq"); } + @Test + public void limitedChannelsAreReleasedOnTimeout() { + CachingConnectionFactory connectionFactory = new CachingConnectionFactory("localhost"); + connectionFactory.setChannelCacheSize(1); + connectionFactory.setChannelCheckoutTimeout(500L); + RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); + AsyncRabbitTemplate asyncRabbitTemplate = new AsyncRabbitTemplate(rabbitTemplate); + asyncRabbitTemplate.setReceiveTimeout(500L); + asyncRabbitTemplate.start(); + + RabbitConverterFuture replyFuture1 = asyncRabbitTemplate.convertSendAndReceive("noReply1"); + RabbitConverterFuture replyFuture2 = asyncRabbitTemplate.convertSendAndReceive("noReply2"); + + assertThatExceptionOfType(ExecutionException.class) + .isThrownBy(() -> replyFuture1.get(10, TimeUnit.SECONDS)) + .withCauseInstanceOf(AmqpReplyTimeoutException.class); + + assertThatExceptionOfType(ExecutionException.class) + .isThrownBy(() -> replyFuture2.get(10, TimeUnit.SECONDS)) + .withCauseInstanceOf(AmqpReplyTimeoutException.class); + + asyncRabbitTemplate.stop(); + connectionFactory.destroy(); + } + private void checkConverterResult(CompletableFuture future, String expected) throws InterruptedException { final CountDownLatch cdl = new CountDownLatch(1); final AtomicReference resultRef = new AtomicReference<>(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchIntegrationTests.java index 4bcb6f92bc..8121e395bb 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 the original author or authors. + * Copyright 2019-2024 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. @@ -25,6 +25,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.batch.SimpleBatchingStrategy; @@ -62,6 +63,11 @@ public class EnableRabbitBatchIntegrationTests { @Autowired private Listener listener; + @BeforeAll + static void setup() { + System.setProperty("spring.amqp.deserialization.trust.all", "true"); + } + @Test public void simpleList() throws InterruptedException { this.template.convertAndSend("batch.1", new Foo("foo")); From 8fc46711773cc4e250f781348a2545c7e55207ea Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 18 Mar 2024 15:27:45 +0000 Subject: [PATCH 429/737] [artifactory-release] Release version 3.1.3 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 59c2ad5c2b..1fc4ad4ca1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.1.3-SNAPSHOT +version=3.1.3 org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true From 650b5d6ac21ed3fc773ce7062e50520e6e6414a2 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 18 Mar 2024 15:27:47 +0000 Subject: [PATCH 430/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 1fc4ad4ca1..4801f1616e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.1.3 +version=3.1.4-SNAPSHOT org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true From 18819e6e2c519d88e4188b4639699a80daf0ad10 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 14:30:45 -0400 Subject: [PATCH 431/737] Bump the development-dependencies group with 1 update Bumps the development-dependencies group with 1 update: com.github.spotbugs. Updates `com.github.spotbugs` from 6.0.8 to 6.0.9 --- updated-dependencies: - dependency-name: com.github.spotbugs dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index bd93d6b9ca..ee2a581d00 100644 --- a/build.gradle +++ b/build.gradle @@ -24,7 +24,7 @@ plugins { id 'io.spring.dependency-management' version '1.1.4' apply false id 'org.antora' version '1.0.0' id 'io.spring.antora.generate-antora-yml' version '0.0.1' - id 'com.github.spotbugs' version '6.0.8' + id 'com.github.spotbugs' version '6.0.9' } description = 'Spring AMQP' From d267af98174b19d7a565c9eaf9cda223ae69bd56 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 28 Mar 2024 11:32:48 -0400 Subject: [PATCH 432/737] GH-2667: Use any `Number` for inbound `x-delay` property Fixes: #2667 The `x-delay` header may arrive as a `Short` from RabbitMQ broker. * Fix `DefaultMessagePropertiesConverter.toMessageProperties()` to deal with a `Number` to extract `long` value from the `x-delay` header **Auto-cherry-pick to `3.0.x`** --- .../support/DefaultMessagePropertiesConverter.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java index a90fae67c1..d088cc2ef4 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java @@ -41,6 +41,8 @@ * @author Gary Russell * @author Soeren Unruh * @author Raylax Grey + * @author Artem Bilan + * * @since 1.0 */ public class DefaultMessagePropertiesConverter implements MessagePropertiesConverter { @@ -92,15 +94,11 @@ public MessageProperties toMessageProperties(BasicProperties source, @Nullable E String key = entry.getKey(); if (MessageProperties.X_DELAY.equals(key)) { Object value = entry.getValue(); - if (value instanceof Integer intValue) { - long receivedDelayLongValue = intValue.longValue(); + if (value instanceof Number numberValue) { + long receivedDelayLongValue = numberValue.longValue(); target.setReceivedDelayLong(receivedDelayLongValue); target.setHeader(key, receivedDelayLongValue); } - else if (value instanceof Long longVal) { - target.setReceivedDelayLong(longVal); - target.setHeader(key, longVal); - } } else { target.setHeader(key, convertLongStringIfNecessary(entry.getValue(), charset)); @@ -180,7 +178,7 @@ private Map convertHeadersIfNecessary(Map header } /** - * Converts a header value to a String if the value type is unsupported by AMQP, also handling values + * Convert a header value to a String if the value type is unsupported by AMQP, also handling values * nested inside Lists or Maps. *

{@code null} values are passed through, although Rabbit client will throw an IllegalArgumentException. * @param valueArg the value. From b96aac675ac495bc4e0534c2f1b88420fad48df2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 10:30:52 -0400 Subject: [PATCH 433/737] Bump the development-dependencies group with 2 updates Bumps the development-dependencies group with 2 updates: [com.github.luben:zstd-jni](https://github.com/luben/zstd-jni) and [io.spring.ge.conventions](https://github.com/spring-io/gradle-enterprise-conventions). Updates `com.github.luben:zstd-jni` from 1.5.5-11 to 1.5.6-1 - [Commits](https://github.com/luben/zstd-jni/compare/v1.5.5-11...v1.5.6-1) Updates `io.spring.ge.conventions` from 0.0.15 to 0.0.16 - [Release notes](https://github.com/spring-io/gradle-enterprise-conventions/releases) - [Commits](https://github.com/spring-io/gradle-enterprise-conventions/compare/v0.0.15...v0.0.16) --- updated-dependencies: - dependency-name: com.github.luben:zstd-jni dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies - dependency-name: io.spring.ge.conventions dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- settings.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index ee2a581d00..234d59187d 100644 --- a/build.gradle +++ b/build.gradle @@ -72,7 +72,7 @@ ext { springRetryVersion = '2.0.5' springVersion = '6.1.5' testcontainersVersion = '1.19.7' - zstdJniVersion = '1.5.5-11' + zstdJniVersion = '1.5.6-1' javaProjects = subprojects - project(':spring-amqp-bom') } diff --git a/settings.gradle b/settings.gradle index 558e054457..2f1b41b03d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,6 @@ plugins { id 'com.gradle.enterprise' version '3.16.2' - id 'io.spring.ge.conventions' version '0.0.15' + id 'io.spring.ge.conventions' version '0.0.16' } rootProject.name = 'spring-amqp-dist' From d800e0d3e2bf34c45ed4dc8368269b154aef2e62 Mon Sep 17 00:00:00 2001 From: seanliu-oss Date: Mon, 1 Apr 2024 14:44:44 -0400 Subject: [PATCH 434/737] Make sure ReceivedDelay is non-negative Related to #2667 The `DefaultMessagePropertiesConverter.toMessageProperties()` may receive an `x-delay` header as negative value. **Auto-cherry-pick to `3.0.x`** --- .../amqp/rabbit/support/DefaultMessagePropertiesConverter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java index d088cc2ef4..7684aab004 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java @@ -95,7 +95,7 @@ public MessageProperties toMessageProperties(BasicProperties source, @Nullable E if (MessageProperties.X_DELAY.equals(key)) { Object value = entry.getValue(); if (value instanceof Number numberValue) { - long receivedDelayLongValue = numberValue.longValue(); + long receivedDelayLongValue = Math.abs(numberValue.longValue()); target.setReceivedDelayLong(receivedDelayLongValue); target.setHeader(key, receivedDelayLongValue); } From 3fb4651ffc1b129fab0fcdbc2d0df2728f76fe3c Mon Sep 17 00:00:00 2001 From: Java4ye <40885447+Java4ye@users.noreply.github.com> Date: Tue, 2 Apr 2024 05:05:57 +0800 Subject: [PATCH 435/737] Simplify logic in checkListenerContainerAware method * More concise and intuitive. * Add author name --- .../rabbit/listener/SimpleMessageListenerContainer.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java index 9a4ffb3705..cfdb9cc5ab 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java @@ -82,6 +82,7 @@ * @author Yansong Ren * @author Tim Bourquin * @author Jeonggi Kim + * @author Java4ye * * @since 1.0 */ @@ -613,12 +614,9 @@ private void checkListenerContainerAware() { Assert.state(expectedQueueNames.size() == queueNames.length, "Listener expects us to be listening on '" + expectedQueueNames + "'; our queues: " + Arrays.asList(queueNames)); - boolean found = false; + boolean found = true; for (String queueName : queueNames) { - if (expectedQueueNames.contains(queueName)) { - found = true; - } - else { + if (!expectedQueueNames.contains(queueName)) { found = false; break; } From 3f4ae51aac1f5050eab910ef28c066e934d154a2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 09:37:37 -0400 Subject: [PATCH 436/737] Bump com.github.luben:zstd-jni from 1.5.6-1 to 1.5.6-2 Bumps [com.github.luben:zstd-jni](https://github.com/luben/zstd-jni) from 1.5.6-1 to 1.5.6-2. - [Commits](https://github.com/luben/zstd-jni/compare/v1.5.6-1...v1.5.6-2) --- updated-dependencies: - dependency-name: com.github.luben:zstd-jni dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 234d59187d..b874142f94 100644 --- a/build.gradle +++ b/build.gradle @@ -72,7 +72,7 @@ ext { springRetryVersion = '2.0.5' springVersion = '6.1.5' testcontainersVersion = '1.19.7' - zstdJniVersion = '1.5.6-1' + zstdJniVersion = '1.5.6-2' javaProjects = subprojects - project(':spring-amqp-bom') } From acd4ab036e14434c331c42a0f2537a4d1d114a65 Mon Sep 17 00:00:00 2001 From: Java4ye <40885447+Java4ye@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:50:31 +0800 Subject: [PATCH 437/737] Refactor getOrSetCorrelationIdAndSetReplyTo method Refactored the `getOrSetCorrelationIdAndSetReplyTo` method for improved readability and code organization. Merged the declaration and initialization of `correlationId` into a single line. Ensured the logic remains unchanged. No functional changes introduced. --- .../springframework/amqp/rabbit/AsyncRabbitTemplate.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java index 30c3f18908..077eacb7de 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java @@ -87,6 +87,7 @@ * * @author Gary Russell * @author Artem Bilan + * @author FengYang Su * * @since 1.6 */ @@ -666,18 +667,14 @@ public void confirm(@NonNull CorrelationData correlationData, boolean ack, @Null private String getOrSetCorrelationIdAndSetReplyTo(Message message, @Nullable AsyncCorrelationData correlationData) { - String correlationId; MessageProperties messageProperties = message.getMessageProperties(); Assert.notNull(messageProperties, "the message properties cannot be null"); - String currentCorrelationId = messageProperties.getCorrelationId(); - if (!StringUtils.hasText(currentCorrelationId)) { + String correlationId = messageProperties.getCorrelationId(); + if (!StringUtils.hasText(correlationId)) { correlationId = correlationData != null ? correlationData.getId() : UUID.randomUUID().toString(); messageProperties.setCorrelationId(correlationId); Assert.isNull(messageProperties.getReplyTo(), "'replyTo' property must be null"); } - else { - correlationId = currentCorrelationId; - } messageProperties.setReplyTo(this.replyAddress); return correlationId; } From 50a06fc0fe952a3192f7ad88b179f8358cc0eb0d Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 8 Apr 2024 14:17:06 -0400 Subject: [PATCH 438/737] GH-2673: Fix memory leak in the `RabbitFuture` Fixes: #2673 When `AsyncRabbitTemplate#sendAndReceive()` is called a got reply, nothing is clearing or canceling a `RabbitFuture.timeoutTask` * Fix `RabbitFuture` overriding `complete(T value)` and `completeExceptionally(Throwable ex)` and call `this.timeoutTask.cancel(true)` **Auto-cherry-pick to `3.0.x`** --- .../amqp/rabbit/RabbitFuture.java | 24 ++++++++++++++++--- .../amqp/rabbit/AsyncRabbitTemplateTests.java | 2 ++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitFuture.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitFuture.java index 17bab1884b..77336202c5 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitFuture.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitFuture.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2024 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. @@ -29,6 +29,8 @@ * @param the type. * * @author Gary Russell + * @author Artem Bilan + * * @since 2.4.7 */ public abstract class RabbitFuture extends CompletableFuture { @@ -74,13 +76,29 @@ Message getRequestMessage() { return this.requestMessage; } + @Override + public boolean complete(T value) { + cancelTimeoutTaskIfAny(); + return super.complete(value); + } + + @Override + public boolean completeExceptionally(Throwable ex) { + cancelTimeoutTaskIfAny(); + return super.completeExceptionally(ex); + } + @Override public boolean cancel(boolean mayInterruptIfRunning) { + cancelTimeoutTaskIfAny(); + this.canceler.accept(this.correlationId, this.channelHolder); + return super.cancel(mayInterruptIfRunning); + } + + private void cancelTimeoutTaskIfAny() { if (this.timeoutTask != null) { this.timeoutTask.cancel(true); } - this.canceler.accept(this.correlationId, this.channelHolder); - return super.cancel(mayInterruptIfRunning); } /** diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java index 4d9a1bca81..1c72c9db77 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java @@ -28,6 +28,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -472,6 +473,7 @@ private Message checkMessageResult(CompletableFuture future, String exp }); assertThat(cdl.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(new String(resultRef.get().getBody())).isEqualTo(expected); + assertThat(TestUtils.getPropertyValue(future, "timeoutTask", Future.class).isCancelled()).isTrue(); return resultRef.get(); } From 12dab68b647c5150042fc13b084d02bef8de2f99 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Apr 2024 02:43:36 +0000 Subject: [PATCH 439/737] Bump io.micrometer:micrometer-bom from 1.12.4 to 1.12.5 (#2680) Bumps [io.micrometer:micrometer-bom](https://github.com/micrometer-metrics/micrometer) from 1.12.4 to 1.12.5. - [Release notes](https://github.com/micrometer-metrics/micrometer/releases) - [Commits](https://github.com/micrometer-metrics/micrometer/compare/v1.12.4...v1.12.5) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b874142f94..c8ebe78846 100644 --- a/build.gradle +++ b/build.gradle @@ -61,7 +61,7 @@ ext { logbackVersion = '1.4.14' lz4Version = '1.8.0' micrometerDocsVersion = '1.0.2' - micrometerVersion = '1.12.4' + micrometerVersion = '1.12.5' micrometerTracingVersion = '1.2.4' mockitoVersion = '5.6.0' rabbitmqStreamVersion = '0.14.0' From daf9154f32f719c546f8835703fbe56453fe8dfc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Apr 2024 02:44:25 +0000 Subject: [PATCH 440/737] Bump com.github.spotbugs in the development-dependencies group (#2679) Bumps the development-dependencies group with 1 update: com.github.spotbugs. Updates `com.github.spotbugs` from 6.0.9 to 6.0.12 --- updated-dependencies: - dependency-name: com.github.spotbugs dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c8ebe78846..d0fe1cc0df 100644 --- a/build.gradle +++ b/build.gradle @@ -24,7 +24,7 @@ plugins { id 'io.spring.dependency-management' version '1.1.4' apply false id 'org.antora' version '1.0.0' id 'io.spring.antora.generate-antora-yml' version '0.0.1' - id 'com.github.spotbugs' version '6.0.9' + id 'com.github.spotbugs' version '6.0.12' } description = 'Spring AMQP' From 3551f8e21e0edb8f32a6387e53f8a70f3a15a2ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Apr 2024 02:45:12 +0000 Subject: [PATCH 441/737] Bump io.projectreactor:reactor-bom from 2023.0.4 to 2023.0.5 (#2682) Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2023.0.4 to 2023.0.5. - [Release notes](https://github.com/reactor/reactor/releases) - [Commits](https://github.com/reactor/reactor/compare/2023.0.4...2023.0.5) --- updated-dependencies: - dependency-name: io.projectreactor:reactor-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d0fe1cc0df..b7b381c9c1 100644 --- a/build.gradle +++ b/build.gradle @@ -66,7 +66,7 @@ ext { mockitoVersion = '5.6.0' rabbitmqStreamVersion = '0.14.0' rabbitmqVersion = '5.19.0' - reactorVersion = '2023.0.4' + reactorVersion = '2023.0.5' snappyVersion = '1.1.10.5' springDataVersion = '2023.1.4' springRetryVersion = '2.0.5' From 545a86710c96717a58c23d8b8fbc749d510eb810 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Apr 2024 02:45:21 +0000 Subject: [PATCH 442/737] Bump org.springframework.data:spring-data-bom from 2023.1.4 to 2023.1.5 (#2683) Bumps [org.springframework.data:spring-data-bom](https://github.com/spring-projects/spring-data-bom) from 2023.1.4 to 2023.1.5. - [Release notes](https://github.com/spring-projects/spring-data-bom/releases) - [Commits](https://github.com/spring-projects/spring-data-bom/compare/2023.1.4...2023.1.5) --- updated-dependencies: - dependency-name: org.springframework.data:spring-data-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b7b381c9c1..1ede5b87e4 100644 --- a/build.gradle +++ b/build.gradle @@ -68,7 +68,7 @@ ext { rabbitmqVersion = '5.19.0' reactorVersion = '2023.0.5' snappyVersion = '1.1.10.5' - springDataVersion = '2023.1.4' + springDataVersion = '2023.1.5' springRetryVersion = '2.0.5' springVersion = '6.1.5' testcontainersVersion = '1.19.7' From f6528caac45a2b5cbeeb9f7b7f999e070a984e6f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Apr 2024 02:46:05 +0000 Subject: [PATCH 443/737] Bump org.springframework:spring-framework-bom from 6.1.5 to 6.1.6 (#2684) Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.1.5 to 6.1.6. - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.1.5...v6.1.6) --- updated-dependencies: - dependency-name: org.springframework:spring-framework-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 1ede5b87e4..b3b28c1643 100644 --- a/build.gradle +++ b/build.gradle @@ -70,7 +70,7 @@ ext { snappyVersion = '1.1.10.5' springDataVersion = '2023.1.5' springRetryVersion = '2.0.5' - springVersion = '6.1.5' + springVersion = '6.1.6' testcontainersVersion = '1.19.7' zstdJniVersion = '1.5.6-2' From 724e0898ce4ff177c7943c1f0ee752db6d4057b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Apr 2024 02:47:26 +0000 Subject: [PATCH 444/737] Bump io.micrometer:micrometer-tracing-bom from 1.2.4 to 1.2.5 (#2681) Bumps [io.micrometer:micrometer-tracing-bom](https://github.com/micrometer-metrics/tracing) from 1.2.4 to 1.2.5. - [Release notes](https://github.com/micrometer-metrics/tracing/releases) - [Commits](https://github.com/micrometer-metrics/tracing/compare/v1.2.4...v1.2.5) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-tracing-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b3b28c1643..eab243b2c2 100644 --- a/build.gradle +++ b/build.gradle @@ -62,7 +62,7 @@ ext { lz4Version = '1.8.0' micrometerDocsVersion = '1.0.2' micrometerVersion = '1.12.5' - micrometerTracingVersion = '1.2.4' + micrometerTracingVersion = '1.2.5' mockitoVersion = '5.6.0' rabbitmqStreamVersion = '0.14.0' rabbitmqVersion = '5.19.0' From c92453ba9a1fe86cef7872399ae59eac04182b7a Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 15 Apr 2024 15:30:08 +0000 Subject: [PATCH 445/737] [artifactory-release] Release version 3.1.4 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 4801f1616e..31bb69ecbe 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.1.4-SNAPSHOT +version=3.1.4 org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true From 603e6c8c09838aff5a8dcf3f9e6e1ab1d3488cde Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 15 Apr 2024 15:30:10 +0000 Subject: [PATCH 446/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 31bb69ecbe..c2056c0ff5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.1.4 +version=3.1.5-SNAPSHOT org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true From b415eafc3889066e2204cfa70a7c084100625818 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 12:00:01 -0400 Subject: [PATCH 447/737] Bump com.github.luben:zstd-jni from 1.5.6-2 to 1.5.6-3 Bumps [com.github.luben:zstd-jni](https://github.com/luben/zstd-jni) from 1.5.6-2 to 1.5.6-3. - [Commits](https://github.com/luben/zstd-jni/compare/v1.5.6-2...v1.5.6-3) --- updated-dependencies: - dependency-name: com.github.luben:zstd-jni dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index eab243b2c2..7f4189e679 100644 --- a/build.gradle +++ b/build.gradle @@ -72,7 +72,7 @@ ext { springRetryVersion = '2.0.5' springVersion = '6.1.6' testcontainersVersion = '1.19.7' - zstdJniVersion = '1.5.6-2' + zstdJniVersion = '1.5.6-3' javaProjects = subprojects - project(':spring-amqp-bom') } From 5e503d1ca79315d15337a0178a0cf27e4060ea2c Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 29 Apr 2024 13:12:47 -0400 Subject: [PATCH 448/737] Migrate to `com.gradle.develocity:3.17.2` Fixes: #2690 Fixes: #2692 * Upgrade to `io.spring.ge.conventions:0.0.17` **Auto-cherry-pick to `3.0.x`** --- settings.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/settings.gradle b/settings.gradle index 2f1b41b03d..93219b9bb3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,6 @@ plugins { - id 'com.gradle.enterprise' version '3.16.2' - id 'io.spring.ge.conventions' version '0.0.16' + id 'com.gradle.develocity' version '3.17.2' + id 'io.spring.ge.conventions' version '0.0.17' } rootProject.name = 'spring-amqp-dist' From 5b07cf1e313b5d17fd9c08adcb5a1631ab31851a Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 29 Apr 2024 15:15:18 -0400 Subject: [PATCH 449/737] Remove redundant Develocity GHA secrets **Auto-cherry-pick to `3.0.x`** --- .github/workflows/ci-snapshot.yml | 2 -- .github/workflows/release.yml | 2 -- 2 files changed, 4 deletions(-) diff --git a/.github/workflows/ci-snapshot.yml b/.github/workflows/ci-snapshot.yml index 98e223d8ca..a94396f772 100644 --- a/.github/workflows/ci-snapshot.yml +++ b/.github/workflows/ci-snapshot.yml @@ -21,8 +21,6 @@ jobs: with: gradleTasks: ${{ github.event_name == 'schedule' && '--rerun-tasks' || '' }} secrets: - GRADLE_ENTERPRISE_CACHE_USER: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} GRADLE_ENTERPRISE_SECRET_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fd2fbeb864..f7e832bdef 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,8 +15,6 @@ jobs: uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-release.yml@main secrets: GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} - GRADLE_ENTERPRISE_CACHE_USER: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} GRADLE_ENTERPRISE_SECRET_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} JF_ARTIFACTORY_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} From 05f2a1a5e747bc4dad19f4fd9783f274a03ae6c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 May 2024 22:48:42 -0400 Subject: [PATCH 450/737] Bump the development-dependencies group with 2 updates (#2694) Bumps the development-dependencies group with 2 updates: io.spring.dependency-management and com.github.spotbugs. Updates `io.spring.dependency-management` from 1.1.4 to 1.1.5 Updates `com.github.spotbugs` from 6.0.12 to 6.0.13 --- updated-dependencies: - dependency-name: io.spring.dependency-management dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies - dependency-name: com.github.spotbugs dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 7f4189e679..e5ef091705 100644 --- a/build.gradle +++ b/build.gradle @@ -21,10 +21,10 @@ plugins { id 'idea' id 'org.ajoberstar.grgit' version '4.1.1' id 'io.spring.nohttp' version '0.0.11' - id 'io.spring.dependency-management' version '1.1.4' apply false + id 'io.spring.dependency-management' version '1.1.5' apply false id 'org.antora' version '1.0.0' id 'io.spring.antora.generate-antora-yml' version '0.0.1' - id 'com.github.spotbugs' version '6.0.12' + id 'com.github.spotbugs' version '6.0.13' } description = 'Spring AMQP' From 894a1f54e1d1c9ccc0f5cad69530f14a512a5998 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 May 2024 22:30:17 -0400 Subject: [PATCH 451/737] Bump com.github.spotbugs in the development-dependencies group (#2697) Bumps the development-dependencies group with 1 update: com.github.spotbugs. Updates `com.github.spotbugs` from 6.0.13 to 6.0.14 --- updated-dependencies: - dependency-name: com.github.spotbugs dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e5ef091705..ffbeaaea5a 100644 --- a/build.gradle +++ b/build.gradle @@ -24,7 +24,7 @@ plugins { id 'io.spring.dependency-management' version '1.1.5' apply false id 'org.antora' version '1.0.0' id 'io.spring.antora.generate-antora-yml' version '0.0.1' - id 'com.github.spotbugs' version '6.0.13' + id 'com.github.spotbugs' version '6.0.14' } description = 'Spring AMQP' From 1e2932bebc747a90171c0e9fc4318a63d661d689 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 May 2024 22:30:42 -0400 Subject: [PATCH 452/737] Bump kotlinVersion from 1.9.23 to 1.9.24 (#2698) Bumps `kotlinVersion` from 1.9.23 to 1.9.24. Updates `org.jetbrains.kotlin:kotlin-gradle-plugin` from 1.9.23 to 1.9.24 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.9.23...v1.9.24) Updates `org.jetbrains.kotlin:kotlin-allopen` from 1.9.23 to 1.9.24 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.9.23...v1.9.24) --- updated-dependencies: - dependency-name: org.jetbrains.kotlin:kotlin-gradle-plugin dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.jetbrains.kotlin:kotlin-allopen dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ffbeaaea5a..c2f9eb8a31 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlinVersion = '1.9.23' + ext.kotlinVersion = '1.9.24' ext.isCI = System.getenv('GITHUB_ACTION') repositories { mavenCentral() From e3fef35232c489f1727efafe5b9bbc043c1f9af0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 May 2024 22:31:04 -0400 Subject: [PATCH 453/737] Bump org.testcontainers:testcontainers-bom from 1.19.7 to 1.19.8 (#2699) Bumps [org.testcontainers:testcontainers-bom](https://github.com/testcontainers/testcontainers-java) from 1.19.7 to 1.19.8. - [Release notes](https://github.com/testcontainers/testcontainers-java/releases) - [Changelog](https://github.com/testcontainers/testcontainers-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/testcontainers/testcontainers-java/compare/1.19.7...1.19.8) --- updated-dependencies: - dependency-name: org.testcontainers:testcontainers-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c2f9eb8a31..585ad5b4bb 100644 --- a/build.gradle +++ b/build.gradle @@ -71,7 +71,7 @@ ext { springDataVersion = '2023.1.5' springRetryVersion = '2.0.5' springVersion = '6.1.6' - testcontainersVersion = '1.19.7' + testcontainersVersion = '1.19.8' zstdJniVersion = '1.5.6-3' javaProjects = subprojects - project(':spring-amqp-bom') From 8b9b6f37d8f29a7ddd283182081fd6e05ae43bfe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 May 2024 22:31:30 -0400 Subject: [PATCH 454/737] Bump com.gradle.develocity from 3.17.2 to 3.17.3 (#2700) Bumps com.gradle.develocity from 3.17.2 to 3.17.3. --- updated-dependencies: - dependency-name: com.gradle.develocity dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index 93219b9bb3..223f8553f5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,5 @@ plugins { - id 'com.gradle.develocity' version '3.17.2' + id 'com.gradle.develocity' version '3.17.3' id 'io.spring.ge.conventions' version '0.0.17' } From e7d5109dadf47ea428dc43aea4532e339402f096 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 18 May 2024 02:50:45 +0000 Subject: [PATCH 455/737] Bump io.micrometer:micrometer-bom from 1.12.5 to 1.12.6 (#2712) Bumps [io.micrometer:micrometer-bom](https://github.com/micrometer-metrics/micrometer) from 1.12.5 to 1.12.6. - [Release notes](https://github.com/micrometer-metrics/micrometer/releases) - [Commits](https://github.com/micrometer-metrics/micrometer/compare/v1.12.5...v1.12.6) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 585ad5b4bb..c3602662ef 100644 --- a/build.gradle +++ b/build.gradle @@ -61,7 +61,7 @@ ext { logbackVersion = '1.4.14' lz4Version = '1.8.0' micrometerDocsVersion = '1.0.2' - micrometerVersion = '1.12.5' + micrometerVersion = '1.12.6' micrometerTracingVersion = '1.2.5' mockitoVersion = '5.6.0' rabbitmqStreamVersion = '0.14.0' From 7441856140cb4514c84d781a48a356e09fa5eeed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 18 May 2024 02:51:30 +0000 Subject: [PATCH 456/737] Bump com.gradle.develocity from 3.17.3 to 3.17.4 (#2709) Bumps com.gradle.develocity from 3.17.3 to 3.17.4. --- updated-dependencies: - dependency-name: com.gradle.develocity dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index 223f8553f5..e0c67ff13c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,5 @@ plugins { - id 'com.gradle.develocity' version '3.17.3' + id 'com.gradle.develocity' version '3.17.4' id 'io.spring.ge.conventions' version '0.0.17' } From 344e265d6c23710436445dd3e479f0b04c46525b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 18 May 2024 02:51:45 +0000 Subject: [PATCH 457/737] Bump org.springframework.data:spring-data-bom from 2023.1.5 to 2023.1.6 (#2708) Bumps [org.springframework.data:spring-data-bom](https://github.com/spring-projects/spring-data-bom) from 2023.1.5 to 2023.1.6. - [Release notes](https://github.com/spring-projects/spring-data-bom/releases) - [Commits](https://github.com/spring-projects/spring-data-bom/compare/2023.1.5...2023.1.6) --- updated-dependencies: - dependency-name: org.springframework.data:spring-data-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c3602662ef..ce67f65107 100644 --- a/build.gradle +++ b/build.gradle @@ -68,7 +68,7 @@ ext { rabbitmqVersion = '5.19.0' reactorVersion = '2023.0.5' snappyVersion = '1.1.10.5' - springDataVersion = '2023.1.5' + springDataVersion = '2023.1.6' springRetryVersion = '2.0.5' springVersion = '6.1.6' testcontainersVersion = '1.19.8' From b34b1bc37de7a3bf8d180485ea7a7baa564303a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 18 May 2024 02:52:03 +0000 Subject: [PATCH 458/737] Bump io.projectreactor:reactor-bom from 2023.0.5 to 2023.0.6 (#2711) Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2023.0.5 to 2023.0.6. - [Release notes](https://github.com/reactor/reactor/releases) - [Commits](https://github.com/reactor/reactor/compare/2023.0.5...2023.0.6) --- updated-dependencies: - dependency-name: io.projectreactor:reactor-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ce67f65107..cf2ee06975 100644 --- a/build.gradle +++ b/build.gradle @@ -66,7 +66,7 @@ ext { mockitoVersion = '5.6.0' rabbitmqStreamVersion = '0.14.0' rabbitmqVersion = '5.19.0' - reactorVersion = '2023.0.5' + reactorVersion = '2023.0.6' snappyVersion = '1.1.10.5' springDataVersion = '2023.1.6' springRetryVersion = '2.0.5' From fdd52e3a8d9b92e1aafb687ff6042288cf9e39e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 18 May 2024 02:52:35 +0000 Subject: [PATCH 459/737] Bump org.springframework:spring-framework-bom from 6.1.6 to 6.1.7 (#2713) Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.1.6 to 6.1.7. - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.1.6...v6.1.7) --- updated-dependencies: - dependency-name: org.springframework:spring-framework-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index cf2ee06975..94ab79f69b 100644 --- a/build.gradle +++ b/build.gradle @@ -70,7 +70,7 @@ ext { snappyVersion = '1.1.10.5' springDataVersion = '2023.1.6' springRetryVersion = '2.0.5' - springVersion = '6.1.6' + springVersion = '6.1.7' testcontainersVersion = '1.19.8' zstdJniVersion = '1.5.6-3' From 20218b4d09b9de6e3847cdfb9f07165530b0cdea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 18 May 2024 02:55:10 +0000 Subject: [PATCH 460/737] Bump io.micrometer:micrometer-tracing-bom from 1.2.5 to 1.2.6 (#2714) Bumps [io.micrometer:micrometer-tracing-bom](https://github.com/micrometer-metrics/tracing) from 1.2.5 to 1.2.6. - [Release notes](https://github.com/micrometer-metrics/tracing/releases) - [Commits](https://github.com/micrometer-metrics/tracing/compare/v1.2.5...v1.2.6) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-tracing-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 94ab79f69b..ab96dfcab4 100644 --- a/build.gradle +++ b/build.gradle @@ -62,7 +62,7 @@ ext { lz4Version = '1.8.0' micrometerDocsVersion = '1.0.2' micrometerVersion = '1.12.6' - micrometerTracingVersion = '1.2.5' + micrometerTracingVersion = '1.2.6' mockitoVersion = '5.6.0' rabbitmqStreamVersion = '0.14.0' rabbitmqVersion = '5.19.0' From 105dfac42fc9b00c3887423de2a5f261cd1b3dfa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 18 May 2024 02:56:08 +0000 Subject: [PATCH 461/737] Bump org.springframework.retry:spring-retry from 2.0.5 to 2.0.6 (#2710) Bumps [org.springframework.retry:spring-retry](https://github.com/spring-projects/spring-retry) from 2.0.5 to 2.0.6. - [Release notes](https://github.com/spring-projects/spring-retry/releases) - [Commits](https://github.com/spring-projects/spring-retry/compare/v2.0.5...v2.0.6) --- updated-dependencies: - dependency-name: org.springframework.retry:spring-retry dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ab96dfcab4..4dee36f69c 100644 --- a/build.gradle +++ b/build.gradle @@ -69,7 +69,7 @@ ext { reactorVersion = '2023.0.6' snappyVersion = '1.1.10.5' springDataVersion = '2023.1.6' - springRetryVersion = '2.0.5' + springRetryVersion = '2.0.6' springVersion = '6.1.7' testcontainersVersion = '1.19.8' zstdJniVersion = '1.5.6-3' From b6409de35db7f48a8fa652682a41aa27ec0c55e7 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 20 May 2024 10:32:13 -0400 Subject: [PATCH 462/737] GH-2715: Fix channel leak in `CachingConnectionFactory` Fixes: #2715 When connection is closed from the broker, there are some channels leak into a cache after reconnection **Auto-cherry-pick to `3.0.x`** --- .../connection/CachingConnectionFactory.java | 55 +++++++------------ 1 file changed, 19 insertions(+), 36 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java index 57fe722693..f14aa989b9 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java @@ -194,7 +194,6 @@ public enum ConfirmType { private final Condition connectionAvailableCondition = this.connectionLock.newCondition(); - private final ActiveObjectCounter inFlightAsyncCloses = new ActiveObjectCounter<>(); private final AtomicBoolean running = new AtomicBoolean(); @@ -1156,7 +1155,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl return null; } else { - physicalClose(channelProxy); + physicalClose(); return null; } } @@ -1237,29 +1236,16 @@ else if (txEnds.contains(methodName)) { } } - private void releasePermitIfNecessary(ChannelProxy proxy) { + private void releasePermitIfNecessary() { if (CachingConnectionFactory.this.channelCheckoutTimeout > 0) { - /* - * Only release a permit if this is a normal close; if the channel is - * in the list, it means we're closing a cached channel (for which a permit - * has already been released). - */ - - this.theConnection.channelListLock.lock(); - try { - if (this.channelList.contains(proxy)) { - return; - } - } - finally { - this.theConnection.channelListLock.unlock(); - } Semaphore permits = CachingConnectionFactory.this.checkoutPermits.get(this.theConnection); if (permits != null) { - permits.release(); - if (logger.isDebugEnabled()) { - logger.debug("Released permit for '" + this.theConnection + "', remaining: " - + permits.availablePermits()); + if (permits.availablePermits() < CachingConnectionFactory.this.channelCacheSize) { + permits.release(); + if (logger.isDebugEnabled()) { + logger.debug("Released permit for '" + this.theConnection + "', remaining: " + + permits.availablePermits()); + } } } else { @@ -1285,11 +1271,8 @@ private void logicalClose(ChannelProxy proxy) throws IOException, TimeoutExcepti if (this.target instanceof PublisherCallbackChannel) { this.target.close(); // emit nacks if necessary } - if (this.channelList.contains(proxy)) { - this.channelList.remove(proxy); - } - else { - releasePermitIfNecessary(proxy); + if (!this.channelList.remove(proxy)) { + releasePermitIfNecessary(); } this.target = null; return; @@ -1328,7 +1311,7 @@ private void returnToCache(ChannelProxy proxy) { // The channel didn't handle confirms, so close it altogether to avoid // memory leaks for pending confirms try { - physicalClose(this.theConnection.channelsAwaitingAcks.remove(this.target)); + physicalClose(); } catch (@SuppressWarnings(UNUSED) Exception e) { } @@ -1352,7 +1335,7 @@ private void doReturnToCache(@Nullable ChannelProxy proxy) { else { if (proxy.isOpen()) { try { - physicalClose(proxy); + physicalClose(); } catch (@SuppressWarnings(UNUSED) Exception e) { } @@ -1372,7 +1355,7 @@ private void cacheOrClose(ChannelProxy proxy) { logger.trace("Cache limit reached: " + this.target); } try { - physicalClose(proxy); + physicalClose(); } catch (@SuppressWarnings(UNUSED) Exception e) { } @@ -1381,8 +1364,8 @@ else if (!alreadyCached) { if (logger.isTraceEnabled()) { logger.trace("Returning cached Channel: " + this.target); } - releasePermitIfNecessary(proxy); this.channelList.addLast(proxy); + releasePermitIfNecessary(); setHighWaterMark(); } } @@ -1399,7 +1382,7 @@ private void setHighWaterMark() { } } - private void physicalClose(ChannelProxy proxy) throws IOException, TimeoutException { + private void physicalClose() throws IOException, TimeoutException { if (logger.isDebugEnabled()) { logger.debug("Closing cached Channel: " + this.target); } @@ -1413,7 +1396,7 @@ private void physicalClose(ChannelProxy proxy) throws IOException, TimeoutExcept (ConfirmType.CORRELATED.equals(CachingConnectionFactory.this.confirmType) || CachingConnectionFactory.this.publisherReturns)) { async = true; - asyncClose(proxy); + asyncClose(); } else { this.target.close(); @@ -1430,12 +1413,12 @@ private void physicalClose(ChannelProxy proxy) throws IOException, TimeoutExcept finally { this.target = null; if (!async) { - releasePermitIfNecessary(proxy); + releasePermitIfNecessary(); } } } - private void asyncClose(ChannelProxy proxy) { + private void asyncClose() { ExecutorService executorService = getChannelsExecutor(); final Channel channel = CachedChannelInvocationHandler.this.target; CachingConnectionFactory.this.inFlightAsyncCloses.add(channel); @@ -1467,7 +1450,7 @@ private void asyncClose(ChannelProxy proxy) { } finally { CachingConnectionFactory.this.inFlightAsyncCloses.release(channel); - releasePermitIfNecessary(proxy); + releasePermitIfNecessary(); } } }); From e3a5d814b062104b0b4353788eb85a36dd54277d Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 20 May 2024 16:32:11 +0000 Subject: [PATCH 463/737] [artifactory-release] Release version 3.1.5 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index c2056c0ff5..6ff7bbe4d5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.1.5-SNAPSHOT +version=3.1.5 org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true From 1bad8ed99118cfb0aff5d21c94dcf6890a6cec0c Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 20 May 2024 16:32:13 +0000 Subject: [PATCH 464/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 6ff7bbe4d5..53f8c87143 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.1.5 +version=3.1.6-SNAPSHOT org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true From 7fd71a2d83e20e236b34e0726141549607d4c51f Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 28 May 2024 10:38:49 -0400 Subject: [PATCH 465/737] Start version `3.2` --- .github/dependabot.yml | 4 +-- gradle.properties | 2 +- src/reference/antora/modules/ROOT/nav.adoc | 1 + .../changes-in-3-1-since-3-0.adoc | 24 +++++++++++++++++ .../antora/modules/ROOT/pages/whats-new.adoc | 27 ++++--------------- 5 files changed, 33 insertions(+), 25 deletions(-) create mode 100644 src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-3-1-since-3-0.adoc diff --git a/.github/dependabot.yml b/.github/dependabot.yml index eea47007ce..ea7fd6cdba 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -33,7 +33,7 @@ updates: - com.github.luben:zstd-jni - package-ecosystem: gradle - target-branch: 3.0.x + target-branch: 3.1.x directory: / schedule: interval: weekly @@ -78,7 +78,7 @@ updates: - '*' - package-ecosystem: github-actions - target-branch: 3.0.x + target-branch: 3.1.x directory: / schedule: interval: weekly diff --git a/gradle.properties b/gradle.properties index 53f8c87143..6bc0422ab1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.1.6-SNAPSHOT +version=3.2.0-SNAPSHOT org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true diff --git a/src/reference/antora/modules/ROOT/nav.adoc b/src/reference/antora/modules/ROOT/nav.adoc index a6a17cb96a..40dd5a27e1 100644 --- a/src/reference/antora/modules/ROOT/nav.adoc +++ b/src/reference/antora/modules/ROOT/nav.adoc @@ -68,6 +68,7 @@ * Change History ** xref:appendix/current-release.adoc[] ** xref:appendix/previous-whats-new.adoc[] +*** xref:appendix/previous-whats-new/changes-in-3-1-since-3-0.adoc[] *** xref:appendix/previous-whats-new/changes-in-3-0-since-2-4.adoc[] *** xref:appendix/previous-whats-new/changes-in-2-4-since-2-3.adoc[] *** xref:appendix/previous-whats-new/message-converter-changes.adoc[] diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-3-1-since-3-0.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-3-1-since-3-0.adoc new file mode 100644 index 0000000000..7c8327fcd2 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-3-1-since-3-0.adoc @@ -0,0 +1,24 @@ +[[changes-in-3-1-since-3-0]] +== Changes in 3.1 Since 3.0 + +[[java-17-spring-framework-6-1]] +=== Java 17, Spring Framework 6.1 + +This version requires Spring Framework 6.1 and Java 17. + +[[x31-exc]] +=== Exclusive Consumer Logging + +Log messages reporting access refusal due to exclusive consumers are now logged at DEBUG level by default. +It remains possible to configure your own logging behavior by setting the `exclusiveConsumerExceptionLogger` and `closeExceptionLogger` properties on the listener container and connection factory respectively. +In addition, the `SimpleMessageListenerContainer` consumer restart after such an exception is now logged at DEBUG level by default (previously INFO). +A new method `logRestart()` has been added to the `ConditionalExceptionLogger` to allow this to be changed. +See xref:amqp/receiving-messages/consumer-events.adoc[Consumer Events] and xref:amqp/connections.adoc#channel-close-logging[Logging Channel Close Events] for more information. + +[[x31-conn-backoff]] +=== Connections Enhancement + +Connection Factory supported backoff policy when creating connection channel. +See xref:amqp/connections.adoc[Choosing a Connection Factory] for more information. + + diff --git a/src/reference/antora/modules/ROOT/pages/whats-new.adoc b/src/reference/antora/modules/ROOT/pages/whats-new.adoc index 4c933d4213..5c8c40f205 100644 --- a/src/reference/antora/modules/ROOT/pages/whats-new.adoc +++ b/src/reference/antora/modules/ROOT/pages/whats-new.adoc @@ -2,27 +2,10 @@ = What's New :page-section-summary-toc: 1 -[[changes-in-3-1-since-3-0]] -== Changes in 3.1 Since 3.0 - -[[java-17-spring-framework-6-1]] -=== Java 17, Spring Framework 6.1 - -This version requires Spring Framework 6.1 and Java 17. - -[[x31-exc]] -=== Exclusive Consumer Logging - -Log messages reporting access refusal due to exclusive consumers are now logged at DEBUG level by default. -It remains possible to configure your own logging behavior by setting the `exclusiveConsumerExceptionLogger` and `closeExceptionLogger` properties on the listener container and connection factory respectively. -In addition, the `SimpleMessageListenerContainer` consumer restart after such an exception is now logged at DEBUG level by default (previously INFO). -A new method `logRestart()` has been added to the `ConditionalExceptionLogger` to allow this to be changed. -See xref:amqp/receiving-messages/consumer-events.adoc[Consumer Events] and <> for more information. - -[[x31-conn-backoff]] -=== Connections Enhancement - -Connection Factory supported backoff policy when creating connection channel. -See xref:amqp/connections.adoc[Choosing a Connection Factory] for more information. +[[changes-in-3-2-since-3-1]] +== Changes in 3.2 Since 3.1 +[[spring-framework-6-2]] +=== Spring Framework 6.1 +This version requires Spring Framework 6.2. \ No newline at end of file From 652ad4f156e43b4b367db10b29461d0ec329e265 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 May 2024 10:44:25 -0400 Subject: [PATCH 466/737] Bump com.github.spotbugs in the development-dependencies group (#2718) Bumps the development-dependencies group with 1 update: com.github.spotbugs. Updates `com.github.spotbugs` from 6.0.14 to 6.0.15 --- updated-dependencies: - dependency-name: com.github.spotbugs dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4dee36f69c..82aa1df552 100644 --- a/build.gradle +++ b/build.gradle @@ -24,7 +24,7 @@ plugins { id 'io.spring.dependency-management' version '1.1.5' apply false id 'org.antora' version '1.0.0' id 'io.spring.antora.generate-antora-yml' version '0.0.1' - id 'com.github.spotbugs' version '6.0.14' + id 'com.github.spotbugs' version '6.0.15' } description = 'Spring AMQP' From 1fee04836d9200b24c7efb31c8502e35f8c3c4dc Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 28 May 2024 11:15:50 -0400 Subject: [PATCH 467/737] Upgrade dependencies for `3.2` generation --- build.gradle | 43 +++++++++++------------ gradle/wrapper/gradle-wrapper.jar | Bin 63721 -> 43453 bytes gradle/wrapper/gradle-wrapper.properties | 4 +-- gradlew.bat | 20 +++++------ 4 files changed, 33 insertions(+), 34 deletions(-) diff --git a/build.gradle b/build.gradle index 82aa1df552..4b6dd3fcbb 100644 --- a/build.gradle +++ b/build.gradle @@ -17,9 +17,8 @@ buildscript { plugins { id 'base' - id 'project-report' id 'idea' - id 'org.ajoberstar.grgit' version '4.1.1' + id 'org.ajoberstar.grgit' version '5.2.2' id 'io.spring.nohttp' version '0.0.11' id 'io.spring.dependency-management' version '1.1.5' apply false id 'org.antora' version '1.0.0' @@ -44,33 +43,33 @@ ext { .filter { f -> f.name.endsWith('.java') || f.name.endsWith('.kt') } } - assertjVersion = '3.24.2' - assertkVersion = '0.27.0' + assertjVersion = '3.26.0' + assertkVersion = '0.28.1' awaitilityVersion = '4.2.1' - commonsCompressVersion = '1.20' - commonsHttpClientVersion = '5.2.3' + commonsCompressVersion = '1.26.2' + commonsHttpClientVersion = '5.3.1' commonsPoolVersion = '2.12.0' hamcrestVersion = '2.2' hibernateValidationVersion = '8.0.1.Final' - jacksonBomVersion = '2.15.4' - jaywayJsonPathVersion = '2.8.0' + jacksonBomVersion = '2.17.1' + jaywayJsonPathVersion = '2.9.0' junit4Version = '4.13.2' - junitJupiterVersion = '5.10.2' - kotlinCoroutinesVersion = '1.7.3' - log4jVersion = '2.21.1' - logbackVersion = '1.4.14' + junitJupiterVersion = '5.11.0-M2' + kotlinCoroutinesVersion = '1.8.1' + log4jVersion = '2.23.1' + logbackVersion = '1.5.6' lz4Version = '1.8.0' micrometerDocsVersion = '1.0.2' - micrometerVersion = '1.12.6' - micrometerTracingVersion = '1.2.6' - mockitoVersion = '5.6.0' - rabbitmqStreamVersion = '0.14.0' - rabbitmqVersion = '5.19.0' - reactorVersion = '2023.0.6' + micrometerVersion = '1.14.0-SNAPSHOT' + micrometerTracingVersion = '1.4.0-SNAPSHOT' + mockitoVersion = '5.12.0' + rabbitmqStreamVersion = '0.15.0' + rabbitmqVersion = '5.21.0' + reactorVersion = '2024.0.0-SNAPSHOT' snappyVersion = '1.1.10.5' - springDataVersion = '2023.1.6' + springDataVersion = '2024.1.0-SNAPSHOT' springRetryVersion = '2.0.6' - springVersion = '6.1.7' + springVersion = '6.2.0-SNAPSHOT' testcontainersVersion = '1.19.8' zstdJniVersion = '1.5.6-3' @@ -560,8 +559,8 @@ dependencies { micrometerDocs "io.micrometer:micrometer-docs-generator:$micrometerDocsVersion" } -def observationInputDir = file("$buildDir/docs/microsources").absolutePath -def generatedDocsDir = file("$buildDir/docs/generated").absolutePath +def observationInputDir = file('build/docs/microsources').absolutePath +def generatedDocsDir = file('build/docs/generated').absolutePath task copyObservation(type: Copy) { from file('spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer').absolutePath diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135c49b765f8051ef9d0a6055ff8e46073d8..e6441136f3d4ba8a0da8d277868979cfbc8ad796 100644 GIT binary patch literal 43453 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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

NnX)EH+Nua)3Y(c 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鯊%1Et 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⁢ zv5o9#E7!G0XmdzfsS{efPNi`1b44~SZ4Z8fuX!I}#8g+(wxzQwUT#Xb2(tbY1+EUhGKoT@KEU9Ktl>_0 z%bjDJg;#*gtJZv!-Zs`?^}v5eKmnbjqlvnSzE@_SP|LG_PJ6CYU+6zY6>92%E+ z=j@TZf-iW4(%U{lnYxQA;7Q!b;^brF8n0D>)`q5>|WDDXLrqYU_tKN2>=#@~OE7grMnNh?UOz-O~6 z6%rHy{#h9K0AT+lDC7q4{hw^|q6*Ry;;L%Q@)Ga}$60_q%D)rv(CtS$CQbpq9|y1e zRSrN4;$Jyl{m5bZw`$8TGvb}(LpY{-cQ)fcyJv7l3S52TLXVDsphtv&aPuDk1OzCA z4A^QtC(!11`IsNx_HnSy?>EKpHJWT^wmS~hc^p^zIIh@9f6U@I2 zC=Mve{j2^)mS#U$e{@Q?SO6%LDsXz@SY+=cK_QMmXBIU)j!$ajc-zLx3V60EXJ!qC zi<%2x8Q24YN+&8U@CIlN zrZkcT9yh%LrlGS9`G)KdP(@9Eo-AQz@8GEFWcb7U=a0H^ZVbLmz{+&M7W(nXJ4sN8 zJLR7eeK(K8`2-}j(T7JsO`L!+CvbueT%izanm-^A1Dn{`1Nw`9P?cq;7no+XfC`K(GO9?O^5zNIt4M+M8LM0=7Gz8UA@Z0N+lg+cX)NfazRu z5D)~HA^(u%w^cz+@2@_#S|u>GpB+j4KzQ^&Wcl9f z&hG#bCA(Yk0D&t&aJE^xME^&E-&xGHhXn%}psEIj641H+Nl-}boj;)Zt*t(4wZ5DN z@GXF$bL=&pBq-#vkTkh>7hl%K5|3 z{`Vn9b$iR-SoGENp}bn4;fR3>9sA%X2@1L3aE9yTra;Wb#_`xWwLSLdfu+PAu+o3| zGVnpzPr=ch{uuoHjtw7+_!L_2;knQ!DuDl0R`|%jr+}jFzXtrHIKc323?JO{l&;VF z*L1+}JU7%QJOg|5|Tc|D8fN zJORAg=_vsy{ak|o);@)Yh8Lkcg@$FG3k@ep36BRa^>~UmnRPziS>Z=`Jb2x*Q#`%A zU*i3&Vg?TluO@X0O;r2Jl6LKLUOVhSqg1*qOt^|8*c7 zo(298@+r$k_wQNGHv{|$tW(T8L+4_`FQ{kEW5Jgg{yf7ey4ss_(SNKfz(N9lx&a;< je(UuV8hP?p&}TPdm1I$XmG#(RzlD&B2izSj9sl%y5~4qc diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4baf5a11d4..381baa9cef 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=9631d53cf3e74bfa726893aee1f8994fee4e060c401335946dba2156f440f24c -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +distributionSha256Sum=544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew.bat b/gradlew.bat index 6689b85bee..7101f8e467 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -43,11 +43,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail From ec35739c84ffde60b2ee66c942d80088b97718ba Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 28 May 2024 20:00:31 -0400 Subject: [PATCH 468/737] GH-2724: Fix `RabbitFuture` for interrupted thread Fixes: #2724 **Auto-cherry-pick to `3.1.x`** --- .../amqp/rabbit/RabbitFuture.java | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitFuture.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitFuture.java index 77336202c5..ab77bcf372 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitFuture.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitFuture.java @@ -78,21 +78,33 @@ Message getRequestMessage() { @Override public boolean complete(T value) { - cancelTimeoutTaskIfAny(); - return super.complete(value); + try { + return super.complete(value); + } + finally { + cancelTimeoutTaskIfAny(); + } } @Override public boolean completeExceptionally(Throwable ex) { - cancelTimeoutTaskIfAny(); - return super.completeExceptionally(ex); + try { + return super.completeExceptionally(ex); + } + finally { + cancelTimeoutTaskIfAny(); + } } @Override public boolean cancel(boolean mayInterruptIfRunning) { - cancelTimeoutTaskIfAny(); this.canceler.accept(this.correlationId, this.channelHolder); - return super.cancel(mayInterruptIfRunning); + try { + return super.cancel(mayInterruptIfRunning); + } + finally { + cancelTimeoutTaskIfAny(); + } } private void cancelTimeoutTaskIfAny() { From dc1e358c3ab18af1c3fa69e7b93e49272833e2b4 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 29 May 2024 15:39:09 -0400 Subject: [PATCH 469/737] GH-2721: Fix batch listener for async return types Fixes: #2721 --- .../AbstractAdaptableMessageListener.java | 14 +++- .../BatchMessagingMessageListenerAdapter.java | 77 ++++++++++++++++++- .../MessagingMessageListenerAdapter.java | 4 +- .../annotation/EnableRabbitKotlinTests.kt | 36 ++++++++- .../pages/amqp/receiving-messages/batch.adoc | 4 +- 5 files changed, 123 insertions(+), 12 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java index 1f72fe8fa3..1eab40f07a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java @@ -314,11 +314,19 @@ public void setDefaultRequeueRejected(boolean defaultRequeueRejected) { this.defaultRequeueRejected = defaultRequeueRejected; } + protected boolean isDefaultRequeueRejected() { + return this.defaultRequeueRejected; + } + @Override public void containerAckMode(AcknowledgeMode mode) { this.isManualAck = AcknowledgeMode.MANUAL.equals(mode); } + protected boolean isManualAck() { + return this.isManualAck; + } + /** * Handle the given exception that arose during listener execution. * The default implementation logs the exception at error level. @@ -413,9 +421,7 @@ private void asyncSuccess(InvocationResult resultArg, Message request, Channel c Object deferredResult) { if (deferredResult == null) { - if (this.logger.isDebugEnabled()) { - this.logger.debug("Async result is null, ignoring"); - } + this.logger.debug("Async result is null, ignoring"); } else { Type returnType = resultArg.getReturnType(); @@ -439,7 +445,7 @@ private void asyncSuccess(InvocationResult resultArg, Message request, Channel c } } - private void basicAck(Message request, Channel channel) { + protected void basicAck(Message request, Channel channel) { try { channel.basicAck(request.getMessageProperties().getDeliveryTag(), false); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapter.java index 3bab4dd7f6..fcedab367f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 the original author or authors. + * Copyright 2019-2024 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. @@ -16,14 +16,17 @@ package org.springframework.amqp.rabbit.listener.adapter; +import java.io.IOException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CompletableFuture; import org.springframework.amqp.rabbit.batch.BatchingStrategy; import org.springframework.amqp.rabbit.batch.SimpleBatchingStrategy; import org.springframework.amqp.rabbit.listener.api.ChannelAwareBatchMessageListener; import org.springframework.amqp.rabbit.listener.api.RabbitListenerErrorHandler; +import org.springframework.amqp.rabbit.listener.support.ContainerUtils; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.amqp.support.converter.MessageConversionException; import org.springframework.lang.Nullable; @@ -37,11 +40,13 @@ * A listener adapter for batch listeners. * * @author Gary Russell + * @author Artem Bilan + * * @since 2.2 * */ public class BatchMessagingMessageListenerAdapter extends MessagingMessageListenerAdapter - implements ChannelAwareBatchMessageListener { + implements ChannelAwareBatchMessageListener { private final MessagingMessageConverterAdapter converterAdapter; @@ -91,13 +96,79 @@ public void onMessageBatch(List messages, } } try { - invokeHandlerAndProcessResult(null, channel, converted); + invokeHandlerAndProcessResult(messages, channel, converted); } catch (Exception e) { throw RabbitExceptionTranslator.convertRabbitAccessException(e); } } + private void invokeHandlerAndProcessResult(List amqpMessages, + Channel channel, Message message) { + + if (logger.isDebugEnabled()) { + logger.debug("Processing [" + message + "]"); + } + InvocationResult result = invokeHandler(null, channel, message); + if (result.getReturnValue() != null) { + handleResult(result, amqpMessages, channel); + } + else { + logger.trace("No result object given - no result to handle"); + } + } + + private void handleResult(InvocationResult resultArg, List amqpMessages, + Channel channel) { + + if (channel != null) { + if (resultArg.getReturnValue() instanceof CompletableFuture completable) { + if (!isManualAck()) { + this.logger.warn("Container AcknowledgeMode must be MANUAL for a Future return type; " + + "otherwise the container will ack the message immediately"); + } + completable.whenComplete((r, t) -> { + if (t == null) { + amqpMessages.forEach((request) -> basicAck(request, channel)); + } + else { + asyncFailure(amqpMessages, channel, t); + } + }); + } + else if (monoPresent && MonoHandler.isMono(resultArg.getReturnValue())) { + if (!isManualAck()) { + this.logger.warn("Container AcknowledgeMode must be MANUAL for a Mono return type" + + "(or Kotlin suspend function); otherwise the container will ack the message immediately"); + } + MonoHandler.subscribe(resultArg.getReturnValue(), + null, + t -> asyncFailure(amqpMessages, channel, t), + () -> amqpMessages.forEach((request) -> basicAck(request, channel))); + } + else { + throw new IllegalStateException("The listener in batch mode does not support replies."); + } + } + else if (this.logger.isWarnEnabled()) { + this.logger.warn("Listener method returned result [" + resultArg + + "]: not generating response message for it because no Rabbit Channel given"); + } + } + + private void asyncFailure(List requests, Channel channel, Throwable t) { + this.logger.error("Future, Mono, or suspend function was completed with an exception for " + requests, t); + for (org.springframework.amqp.core.Message request : requests) { + try { + channel.basicNack(request.getMessageProperties().getDeliveryTag(), false, + ContainerUtils.shouldRequeue(isDefaultRequeueRejected(), t, this.logger)); + } + catch (IOException e) { + this.logger.error("Failed to nack message", e); + } + } + } + @Override protected Message toMessagingMessage(org.springframework.amqp.core.Message amqpMessage) { if (this.batchingStrategy.canDebatch(amqpMessage.getMessageProperties())) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java index 6e3579f8c3..a1318c6904 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java @@ -267,8 +267,8 @@ protected Message toMessagingMessage(org.springframework.amqp.core.Message am * @param message the messaging message. * @return the result of invoking the handler. */ - private InvocationResult invokeHandler(@Nullable org.springframework.amqp.core.Message amqpMessage, Channel channel, - Message message) { + protected InvocationResult invokeHandler(@Nullable org.springframework.amqp.core.Message amqpMessage, + Channel channel, Message message) { try { if (amqpMessage == null) { diff --git a/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt b/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt index 397fe765e8..c1746759e9 100644 --- a/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt +++ b/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt @@ -17,6 +17,7 @@ package org.springframework.amqp.rabbit.annotation import assertk.assertThat +import assertk.assertions.containsOnly import assertk.assertions.isEqualTo import assertk.assertions.isTrue import org.junit.jupiter.api.Test @@ -40,6 +41,7 @@ import org.springframework.test.annotation.DirtiesContext import org.springframework.test.context.junit.jupiter.SpringJUnitConfig import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean /** * Kotlin Annotated listener tests. @@ -51,7 +53,7 @@ import java.util.concurrent.TimeUnit * */ @SpringJUnitConfig -@RabbitAvailable(queues = ["kotlinQueue", "kotlinQueue1", "kotlinReplyQueue"]) +@RabbitAvailable(queues = ["kotlinQueue", "kotlinBatchQueue", "kotlinQueue1", "kotlinReplyQueue"]) @DirtiesContext class EnableRabbitKotlinTests { @@ -69,13 +71,22 @@ class EnableRabbitKotlinTests { .isEqualTo("class java.lang.String") } + @Test + fun `listen for batch`() { + val template = RabbitTemplate(this.config.cf()) + template.convertAndSend("kotlinBatchQueue", "test1") + template.convertAndSend("kotlinBatchQueue", "test2") + assertThat(this.config.batchReceived.await(10, TimeUnit.SECONDS)).isTrue() + assertThat(this.config.batch).containsOnly("test1", "test2") + } + @Test fun `send and wait for consume with EH`() { val template = RabbitTemplate(this.config.cf()) template.convertAndSend("kotlinQueue1", "test") assertThat(this.config.ehLatch.await(10, TimeUnit.SECONDS)).isTrue() val reply = template.receiveAndConvert("kotlinReplyQueue", 10_000) - assertThat(reply).isEqualTo("error processed"); + assertThat(reply).isEqualTo("error processed") } @Configuration @@ -87,6 +98,17 @@ class EnableRabbitKotlinTests { return data.uppercase() } + val batchReceived = CountDownLatch(1) + + lateinit var batch: List + + @RabbitListener(id = "batch", queues = ["kotlinBatchQueue"], + containerFactory = "batchRabbitListenerContainerFactory") + suspend fun receiveBatch(messages: List) { + batchReceived.countDown() + batch = messages + } + @Bean fun rabbitListenerContainerFactory(cf: CachingConnectionFactory) = SimpleRabbitListenerContainerFactory().also { @@ -95,6 +117,16 @@ class EnableRabbitKotlinTests { it.setConnectionFactory(cf) } + @Bean + fun batchRabbitListenerContainerFactory(cf: CachingConnectionFactory) = + SimpleRabbitListenerContainerFactory().also { + it.setAcknowledgeMode(AcknowledgeMode.MANUAL) + it.setConsumerBatchEnabled(true) + it.setDeBatchingEnabled(true) + it.setBatchSize(3) + it.setConnectionFactory(cf) + } + @Bean fun cf() = CachingConnectionFactory(RabbitAvailableCondition.getBrokerRunning().connectionFactory) diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/batch.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/batch.adoc index 80ad49c2b2..5a6ae97838 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/batch.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/batch.adoc @@ -1,7 +1,7 @@ [[receiving-batch]] = @RabbitListener with Batching -When receiving a xref:amqp/sending-messages.adoc#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. +When receiving xref:amqp/sending-messages.adoc#template-batching[a batch] of messages, the de-batching is normally performed by the container, and the listener is invoked with one message 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` or `Collection`: [source, java] @@ -88,3 +88,5 @@ When `consumerBatchEnabled` is `true`, the listener **must** be a batch listener Starting with version 3.0, listener methods can consume `Collection` or `List`. +NOTE: The listener in batch mode does not support replies since there might not be a correlation between messages in the batch and single reply produced. +The xref:amqp/receiving-messages/async-returns.adoc[asynchronous return types] are still supported with batch listeners. \ No newline at end of file From dd22abb27eacc40288215ee4f7b2d0bc3a28edc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Michael?= Date: Thu, 30 May 2024 16:06:14 +0200 Subject: [PATCH 470/737] Restrict `getDeclarablesByType()` to `Declarable` --- .../main/java/org/springframework/amqp/core/Declarables.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/Declarables.java b/spring-amqp/src/main/java/org/springframework/amqp/core/Declarables.java index a5892ef946..e2624dbdc6 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/Declarables.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/Declarables.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2021 the original author or authors. + * Copyright 2018-2024 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. @@ -59,7 +59,7 @@ public Collection getDeclarables() { * @return the filtered list. * @since 2.2 */ - public List getDeclarablesByType(Class type) { + public List getDeclarablesByType(Class type) { return this.declarables.stream() .filter(type::isInstance) .map(type::cast) From cf2f4e780676cee6510fa63fe21ec895319b35a2 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 30 May 2024 15:48:54 -0400 Subject: [PATCH 471/737] Upgrade Antora resources --- build.gradle | 4 ++-- src/reference/antora/antora-playbook.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 4b6dd3fcbb..aef0873dd3 100644 --- a/build.gradle +++ b/build.gradle @@ -86,8 +86,8 @@ antora { '@antora/atlas-extension': '1.0.0-alpha.1', '@antora/collector-extension': '1.0.0-alpha.3', '@asciidoctor/tabs': '1.0.0-beta.3', - '@springio/antora-extensions': '1.4.2', - '@springio/asciidoctor-extensions': '1.0.0-alpha.8', + '@springio/antora-extensions': '1.11.1', + '@springio/asciidoctor-extensions': '1.0.0-alpha.10', ] } diff --git a/src/reference/antora/antora-playbook.yml b/src/reference/antora/antora-playbook.yml index 3d0e26ceb7..ab27152843 100644 --- a/src/reference/antora/antora-playbook.yml +++ b/src/reference/antora/antora-playbook.yml @@ -42,4 +42,4 @@ runtime: format: pretty ui: bundle: - url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.10/ui-bundle.zip + url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.15/ui-bundle.zip From 53b94691b6fb4540791d4164b3c7203c0fb4633e Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 31 May 2024 11:15:55 -0400 Subject: [PATCH 472/737] Use root `@springio/antora-extensions` for docs build See more info in: https://github.com/spring-io/antora-extensions **Auto-cherry-pick to `3.1.x`** --- src/reference/antora/antora-playbook.yml | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/src/reference/antora/antora-playbook.yml b/src/reference/antora/antora-playbook.yml index ab27152843..3b67a1e807 100644 --- a/src/reference/antora/antora-playbook.yml +++ b/src/reference/antora/antora-playbook.yml @@ -1,18 +1,9 @@ antora: - extensions: - - '@springio/antora-extensions/partial-build-extension' - - # atlas-extension must be before latest-version-extension so the latest versions are applied to imported versions - - '@antora/atlas-extension' - - require: '@springio/antora-extensions/latest-version-extension' - - require: '@springio/antora-extensions/inject-collector-cache-config-extension' - - '@antora/collector-extension' - - require: '@springio/antora-extensions/root-component-extension' - root_component_name: 'amqp' - # FIXME: Run antora once using this extension to migrate to the Asciidoc Tabs syntax - # and then remove this extension - - require: '@springio/antora-extensions/tabs-migration-extension' - unwrap_example_block: always - save_result: true + antora: + extensions: + - require: '@springio/antora-extensions' + root_component_name: 'amqp' + site: title: Spring AMQP url: https://docs.spring.io/spring-amqp/reference/ From 556dda5387821f51d001c65d6a28b5ca10bfff17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20H=C3=A4user?= Date: Fri, 31 May 2024 20:49:51 +0200 Subject: [PATCH 473/737] GH-2572: Treat ConfirmType.SIMPLE same as ConfirmType.CORRELATED Fixes: #2572 * GH-2572 Do not close channel during invoke call * GH-2571 Adapt condition and add tests * GH-2571 Handle SIMPLE case in `CachingConnectionFactory::isPublisherConfirms` * GH-2571 Remove obsolete tests --- .../connection/CachingConnectionFactory.java | 2 +- .../amqp/rabbit/connection/RabbitUtils.java | 2 +- .../amqp/rabbit/core/RabbitTemplate.java | 2 +- .../CachingConnectionFactoryTests.java | 26 +++++++++- .../amqp/rabbit/core/RabbitTemplateTests.java | 50 ++++++++++++++++++- 5 files changed, 77 insertions(+), 5 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java index f14aa989b9..7ec224b99e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java @@ -393,7 +393,7 @@ public void setConnectionLimit(int connectionLimit) { @Override public boolean isPublisherConfirms() { - return ConfirmType.CORRELATED.equals(this.confirmType); + return !ConfirmType.NONE.equals(this.confirmType); } @Override diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java index 768f5e66b8..1a055fed30 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java index 866116b073..1a0d74520c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java index 530e93f324..ee45b7ad61 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -1967,4 +1967,28 @@ public void onShutDown(ShutdownSignalException signal) { assertThat(chanShutDown.get()).isFalse(); } + @Test + void isPublisherConfirmsHandlesSimple() { + CachingConnectionFactory ccf = new CachingConnectionFactory("someHost", 1234); + ccf.setPublisherConfirmType(ConfirmType.SIMPLE); + + assertThat(ccf.isPublisherConfirms()).isTrue(); + } + + @Test + void isPublisherConfirmsHandlesCorrelated() { + CachingConnectionFactory ccf = new CachingConnectionFactory("someHost", 1234); + ccf.setPublisherConfirmType(ConfirmType.CORRELATED); + + assertThat(ccf.isPublisherConfirms()).isTrue(); + } + + @Test + void isPublisherConfirmsHandlesNone() { + CachingConnectionFactory ccf = new CachingConnectionFactory("someHost", 1234); + ccf.setPublisherConfirmType(ConfirmType.NONE); + + assertThat(ccf.isPublisherConfirms()).isFalse(); + } + } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateTests.java index 2ceafba40c..cc8683616e 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -559,6 +559,54 @@ public void testPublisherConnWithInvoke() { verify(conn).createChannel(false); } + @Test + public void testPublisherConnWithInvokePhysicallyCloses() { + RabbitUtils.clearPhysicalCloseRequired(); + + org.springframework.amqp.rabbit.connection.ConnectionFactory cf = mock( + org.springframework.amqp.rabbit.connection.ConnectionFactory.class); + org.springframework.amqp.rabbit.connection.ConnectionFactory pcf = mock( + org.springframework.amqp.rabbit.connection.ConnectionFactory.class); + given(cf.getPublisherConnectionFactory()).willReturn(pcf); + given(pcf.isPublisherConfirms()).willReturn(false); + + RabbitTemplate template = new RabbitTemplate(cf); + template.setUsePublisherConnection(true); + org.springframework.amqp.rabbit.connection.Connection conn = mock( + org.springframework.amqp.rabbit.connection.Connection.class); + ChannelProxy channel = mock(ChannelProxy.class); + given(pcf.createConnection()).willReturn(conn); + given(conn.isOpen()).willReturn(true); + given(conn.createChannel(false)).willReturn(channel); + template.invoke(t -> null); + + assertThat(RabbitUtils.isPhysicalCloseRequired()).isTrue(); + } + + @Test + public void testPublisherConnWithInvokeAndPublisherConfirmations() { + RabbitUtils.clearPhysicalCloseRequired(); + + org.springframework.amqp.rabbit.connection.ConnectionFactory cf = mock( + org.springframework.amqp.rabbit.connection.ConnectionFactory.class); + org.springframework.amqp.rabbit.connection.ConnectionFactory pcf = mock( + org.springframework.amqp.rabbit.connection.ConnectionFactory.class); + given(cf.getPublisherConnectionFactory()).willReturn(pcf); + given(pcf.isPublisherConfirms()).willReturn(true); + + RabbitTemplate template = new RabbitTemplate(cf); + template.setUsePublisherConnection(true); + org.springframework.amqp.rabbit.connection.Connection conn = mock( + org.springframework.amqp.rabbit.connection.Connection.class); + ChannelProxy channel = mock(ChannelProxy.class); + given(pcf.createConnection()).willReturn(conn); + given(conn.isOpen()).willReturn(true); + given(conn.createChannel(false)).willReturn(channel); + template.invoke(t -> null); + + assertThat(RabbitUtils.isPhysicalCloseRequired()).isFalse(); + } + @Test public void testPublisherConnWithInvokeInTx() { org.springframework.amqp.rabbit.connection.ConnectionFactory cf = mock( From 2bb4f297931ac39a49d320bd6c81ddf4797c5562 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 3 Jun 2024 10:49:32 -0400 Subject: [PATCH 474/737] Do not close channel when `ConfirmType.SIMPLE` is used for publisher confirmations Related to: #2572 --- .../amqp/rabbit/connection/CachingConnectionFactory.java | 2 +- .../org/springframework/amqp/rabbit/core/RabbitTemplate.java | 2 +- .../amqp/rabbit/connection/CachingConnectionFactoryTests.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java index 7ec224b99e..f14aa989b9 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java @@ -393,7 +393,7 @@ public void setConnectionLimit(int connectionLimit) { @Override public boolean isPublisherConfirms() { - return !ConfirmType.NONE.equals(this.confirmType); + return ConfirmType.CORRELATED.equals(this.confirmType); } @Override diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java index 1a0d74520c..dc33e807d8 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java @@ -2344,7 +2344,7 @@ public T invoke(OperationsCallback action, @Nullable com.rabbitmq.client. if (channel == null) { throw new IllegalStateException("Connection returned a null channel"); } - if (!connectionFactory.isPublisherConfirms()) { + if (!connectionFactory.isPublisherConfirms() && !connectionFactory.isSimplePublisherConfirms()) { RabbitUtils.setPhysicalCloseRequired(channel, true); } this.dedicatedChannels.set(channel); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java index ee45b7ad61..66dc8013ec 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java @@ -1972,7 +1972,7 @@ void isPublisherConfirmsHandlesSimple() { CachingConnectionFactory ccf = new CachingConnectionFactory("someHost", 1234); ccf.setPublisherConfirmType(ConfirmType.SIMPLE); - assertThat(ccf.isPublisherConfirms()).isTrue(); + assertThat(ccf.isPublisherConfirms()).isFalse(); } @Test From d8bc0c603419110a4f6527efd69f7548ea3db09f Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 3 Jun 2024 13:18:06 -0400 Subject: [PATCH 475/737] GH-2727: Remove old deprecations; fix tests for new API Fixes: #2727 --- .../springframework/amqp/core/AmqpAdmin.java | 14 +----- .../amqp/core/MessageProperties.java | 50 ------------------- .../amqp/core/QueueBuilder.java | 16 +----- .../amqp/rabbit/core/RabbitAdmin.java | 20 +------- .../listener/BlockingQueueConsumer.java | 12 ----- .../MessagingMessageListenerAdapter.java | 28 ++++------- .../api/RabbitListenerErrorHandler.java | 26 ++-------- .../EnableRabbitIntegrationTests.java | 10 ++-- .../EnableRabbitReturnTypesTests.java | 4 +- .../MessagingMessageListenerAdapterTests.java | 32 ++++-------- .../annotation/EnableRabbitKotlinTests.kt | 2 +- 11 files changed, 34 insertions(+), 180 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpAdmin.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpAdmin.java index 76f7e8b6f4..0968cf9a08 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpAdmin.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpAdmin.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -17,7 +17,6 @@ package org.springframework.amqp.core; import java.util.Collections; -import java.util.Map; import java.util.Properties; import java.util.Set; @@ -130,17 +129,6 @@ public interface AmqpAdmin { @Nullable QueueInformation getQueueInfo(String queueName); - /** - * Return the manually declared AMQP objects. - * @return the manually declared AMQP objects. - * @since 2.4.13 - * @deprecated in favor of {@link #getManualDeclarableSet()}. - */ - @Deprecated - default Map getManualDeclarables() { - return Collections.emptyMap(); - } - /** * Return the manually declared AMQP objects. * @return the manually declared AMQP objects. diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java index f999acc0a3..2aa516d7db 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java @@ -356,32 +356,6 @@ public String getReceivedRoutingKey() { return this.receivedRoutingKey; } - /** - * When a delayed message exchange is used the x-delay header on a - * received message contains the delay. - * @return the received delay. - * @since 1.6 - * @deprecated in favor of {@link #getReceivedDelayLong()} - * @see #getDelay() - */ - @Deprecated(since = "3.1.2", forRemoval = true) - public Integer getReceivedDelay() { - Long receivedDelay = getReceivedDelayLong(); - return receivedDelay != null ? Math.toIntExact(receivedDelay) : null; - } - - /** - * When a delayed message exchange is used the x-delay header on a - * received message contains the delay. - * @param receivedDelay the received delay. - * @since 1.6 - * @deprecated in favor of {@link #setReceivedDelayLong(Long)} - */ - @Deprecated(since = "3.1.2", forRemoval = true) - public void setReceivedDelay(Integer receivedDelay) { - setReceivedDelayLong(receivedDelay != null ? receivedDelay.longValue() : null); - } - /** * When a delayed message exchange is used the x-delay header on a * received message contains the delay. @@ -466,30 +440,6 @@ public void setConsumerQueue(String consumerQueue) { this.consumerQueue = consumerQueue; } - /** - * The x-delay header (outbound). - * @return the delay. - * @since 1.6 - * @deprecated in favor of {@link #getDelayLong()} - * @see #getReceivedDelay() - */ - @Deprecated(since = "3.1.2", forRemoval = true) - public Integer getDelay() { - Long delay = getDelayLong(); - return delay != null ? Math.toIntExact(delay) : null; - } - - /** - * Set the x-delay header. - * @param delay the delay. - * @since 1.6 - * @deprecated in favor of {@link #setDelayLong(Long)} - */ - @Deprecated(since = "3.1.2", forRemoval = true) - public void setDelay(Integer delay) { - setDelayLong(delay != null ? delay.longValue() : null); - } - /** * Get the x-delay header long value. * @return the delay. diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/QueueBuilder.java b/spring-amqp/src/main/java/org/springframework/amqp/core/QueueBuilder.java index 122a7ea11d..37a622ff63 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/QueueBuilder.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/QueueBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 the original author or authors. + * Copyright 2016-2024 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. @@ -144,20 +144,6 @@ public QueueBuilder expires(int expires) { return withArgument("x-expires", expires); } - /** - * Set the number of (ready) messages allowed in the queue before it starts to drop - * them. - * @param count the number of (ready) messages allowed. - * @return the builder. - * @since 2.2 - * @deprecated in favor of {@link #maxLength(long)}. - * @see #overflow(Overflow) - */ - @Deprecated - public QueueBuilder maxLength(int count) { - return withArgument("x-max-length", count); - } - /** * Set the number of (ready) messages allowed in the queue before it starts to drop * them. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java index a88392b64b..b86dc8063a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -743,24 +743,6 @@ public void resetAllManualDeclarations() { this.manualDeclarables.clear(); } - @Override - @Deprecated - public Map getManualDeclarables() { - Map declarables = new HashMap<>(); - this.manualDeclarables.forEach(declarable -> { - if (declarable instanceof Exchange exch) { - declarables.put(exch.getName(), declarable); - } - else if (declarable instanceof Queue queue) { - declarables.put(queue.getName(), declarable); - } - else if (declarable instanceof Binding) { - declarables.put(declarable.toString(), declarable); - } - }); - return declarables; - } - @Override public Set getManualDeclarableSet() { return Collections.unmodifiableSet(this.manualDeclarables); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java index 0d14eebaef..dd1d6647bc 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java @@ -874,18 +874,6 @@ public void rollbackOnExceptionIfNecessary(Throwable ex, long tag) { } } - /** - * Perform a commit or message acknowledgement, as appropriate. - * NOTE: This method was never been intended tobe public. - * @param localTx Whether the channel is locally transacted. - * @return true if at least one delivery tag exists. - * @deprecated in favor of {@link #commitIfNecessary(boolean, boolean)} - */ - @Deprecated(forRemoval = true, since = "3.1.2") - public boolean commitIfNecessary(boolean localTx) { - return commitIfNecessary(localTx, false); - } - /** * Perform a commit or message acknowledgement, as appropriate. * NOTE: This method was never been intended tobe public. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java index a1318c6904..47ffbed9ee 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java @@ -28,7 +28,6 @@ import org.springframework.amqp.rabbit.listener.api.RabbitListenerErrorHandler; import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; import org.springframework.amqp.support.AmqpHeaderMapper; -import org.springframework.amqp.support.AmqpHeaders; import org.springframework.amqp.support.converter.MessageConversionException; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.amqp.support.converter.MessagingMessageConverter; @@ -41,7 +40,6 @@ import org.springframework.messaging.handler.annotation.Header; import org.springframework.messaging.handler.annotation.Headers; import org.springframework.messaging.handler.annotation.Payload; -import org.springframework.messaging.support.MessageBuilder; import org.springframework.util.Assert; import com.rabbitmq.client.Channel; @@ -179,14 +177,7 @@ private void handleException(org.springframework.amqp.core.Message amqpMessage, if (this.errorHandler != null) { try { - Message messageWithChannel = null; - if (message != null) { - messageWithChannel = MessageBuilder.fromMessage(message) - // TODO won't be necessary starting with version 3.2 - .setHeader(AmqpHeaders.CHANNEL, channel) - .build(); - } - Object errorResult = this.errorHandler.handleError(amqpMessage, channel, messageWithChannel, e); + Object errorResult = this.errorHandler.handleError(amqpMessage, channel, message, e); if (errorResult != null) { Object payload = message == null ? null : message.getPayload(); InvocationResult invResult = payload == null @@ -240,9 +231,9 @@ private void returnOrThrow(org.springframework.amqp.core.Message amqpMessage, Ch Object payload = message == null ? null : message.getPayload(); try { handleResult(new InvocationResult(new RemoteInvocationResult(throwableToReturn), null, - payload == null ? Object.class : this.handlerAdapter.getReturnTypeFor(payload), - this.handlerAdapter.getBean(), - payload == null ? null : this.handlerAdapter.getMethodFor(payload)), + payload == null ? Object.class : this.handlerAdapter.getReturnTypeFor(payload), + this.handlerAdapter.getBean(), + payload == null ? null : this.handlerAdapter.getMethodFor(payload)), amqpMessage, channel, message); } catch (ReplyFailureException rfe) { @@ -405,8 +396,8 @@ private Type determineInferredType() { // NOSONAR - complexity boolean isPayload = methodParameter.hasParameterAnnotation(Payload.class); if (isHeaderOrHeaders && isPayload && MessagingMessageListenerAdapter.this.logger.isWarnEnabled()) { MessagingMessageListenerAdapter.this.logger.warn(this.method.getName() - + ": Cannot annotate a parameter with both @Header and @Payload; " - + "ignored for payload conversion"); + + ": Cannot annotate a parameter with both @Header and @Payload; " + + "ignored for payload conversion"); } if (isEligibleParameter(methodParameter) // NOSONAR && (!isHeaderOrHeaders || isPayload) && !(isHeaderOrHeaders && isPayload)) { @@ -416,7 +407,7 @@ private Type determineInferredType() { // NOSONAR - complexity if (this.isBatch && !this.isCollection) { throw new IllegalStateException( "Mis-configuration; a batch listener must consume a List or " - + "Collection for method: " + this.method); + + "Collection for method: " + this.method); } } @@ -467,8 +458,8 @@ private Type extractGenericParameterTypFromMethodParameter(MethodParameter metho } else if (this.isBatch && ((parameterizedType.getRawType().equals(List.class) - || parameterizedType.getRawType().equals(Collection.class)) - && parameterizedType.getActualTypeArguments().length == 1)) { + || parameterizedType.getRawType().equals(Collection.class)) + && parameterizedType.getActualTypeArguments().length == 1)) { this.isCollection = true; Type paramType = parameterizedType.getActualTypeArguments()[0]; @@ -487,6 +478,7 @@ else if (this.isBatch } return genericParameterType; } + } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/RabbitListenerErrorHandler.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/RabbitListenerErrorHandler.java index 3a01921f2e..53eb43c6d2 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/RabbitListenerErrorHandler.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/RabbitListenerErrorHandler.java @@ -27,28 +27,14 @@ * listener container's error handler. * * @author Gary Russell + * @author Artem Bilan + * * @since 2.0 * */ @FunctionalInterface public interface RabbitListenerErrorHandler { - /** - * Handle the error. If an exception is not thrown, the return value is returned to - * the sender using normal {@code replyTo/@SendTo} semantics. - * @param amqpMessage the raw message received. - * @param message the converted spring-messaging message (if available). - * @param exception the exception the listener threw, wrapped in a - * {@link ListenerExecutionFailedException}. - * @return the return value to be sent to the sender. - * @throws Exception an exception which may be the original or different. - * @deprecated in favor of - * {@link #handleError(Message, Channel, org.springframework.messaging.Message, ListenerExecutionFailedException)} - */ - @Deprecated(forRemoval = true, since = "3.1.3") - Object handleError(Message amqpMessage, @Nullable org.springframework.messaging.Message message, - ListenerExecutionFailedException exception) throws Exception; // NOSONAR - /** * Handle the error. If an exception is not thrown, the return value is returned to * the sender using normal {@code replyTo/@SendTo} semantics. @@ -61,12 +47,8 @@ Object handleError(Message amqpMessage, @Nullable org.springframework.messaging. * @throws Exception an exception which may be the original or different. * @since 3.1.3 */ - @SuppressWarnings("deprecation") - default Object handleError(Message amqpMessage, Channel channel, + Object handleError(Message amqpMessage, Channel channel, @Nullable org.springframework.messaging.Message message, - ListenerExecutionFailedException exception) throws Exception { // NOSONAR - - return handleError(amqpMessage, message, exception); - } + ListenerExecutionFailedException exception) throws Exception; } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java index 856dc04825..c59921a711 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java @@ -1907,12 +1907,12 @@ public MyService myService() { @Bean public RabbitListenerErrorHandler alwaysBARHandler() { - return (msg, springMsg, ex) -> "BAR"; + return (msg, channel, springMsg, ex) -> "BAR"; } @Bean public RabbitListenerErrorHandler upcaseAndRepeatErrorHandler() { - return (msg, springMsg, ex) -> { + return (msg, channel, springMsg, ex) -> { String payload = ((Bar) springMsg.getPayload()).field.toUpperCase(); return payload + payload + " " + ex.getCause().getMessage(); }; @@ -1920,15 +1920,15 @@ public RabbitListenerErrorHandler upcaseAndRepeatErrorHandler() { @Bean public RabbitListenerErrorHandler throwANewException() { - return (msg, springMsg, ex) -> { - this.errorHandlerChannel = springMsg.getHeaders().get(AmqpHeaders.CHANNEL, Channel.class); + return (msg, channel, springMsg, ex) -> { + this.errorHandlerChannel = channel; throw new RuntimeException("from error handler", ex.getCause()); }; } @Bean public RabbitListenerErrorHandler throwWrappedValidationException() { - return (msg, springMsg, ex) -> { + return (msg, channel, springMsg, ex) -> { throw new RuntimeException("argument validation failed", ex); }; } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitReturnTypesTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitReturnTypesTests.java index da80c5e2c6..a99fb81402 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitReturnTypesTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitReturnTypesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2020 the original author or authors. + * Copyright 2019-2024 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. @@ -143,7 +143,7 @@ public ReplyPostProcessor rpp() { @Bean public RabbitListenerErrorHandler rleh() { - return (amqpMessage, message, exception) -> null; + return (amqpMessage, channel, message, exception) -> null; } @RabbitListener(queues = "EnableRabbitReturnTypesTests.1", admin = "#{@admin}", diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapterTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapterTests.java index a268fb295e..157e999a0f 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapterTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * Copyright 2014-2024 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. @@ -284,17 +284,10 @@ void errorHandlerAfterConversionEx() throws Exception { Channel channel = mock(Channel.class); AtomicBoolean ehCalled = new AtomicBoolean(); MessagingMessageListenerAdapter listener = getSimpleInstance("fail", - new RabbitListenerErrorHandler() { - - @Override - public Object handleError(org.springframework.amqp.core.Message amqpMessage, Message message, - ListenerExecutionFailedException exception) throws Exception { - - ehCalled.set(true); - return null; - } - - }, false, String.class); + (amqpMessage, channel1, message1, exception) -> { + ehCalled.set(true); + return null; + }, false, String.class); listener.setMessageConverter(new MessageConverter() { @Override @@ -319,17 +312,10 @@ void errorHandlerAfterConversionExWithResult() throws Exception { Channel channel = mock(Channel.class); AtomicBoolean ehCalled = new AtomicBoolean(); MessagingMessageListenerAdapter listener = getSimpleInstance("fail", - new RabbitListenerErrorHandler() { - - @Override - public Object handleError(org.springframework.amqp.core.Message amqpMessage, Message message, - ListenerExecutionFailedException exception) throws Exception { - - ehCalled.set(true); - return "foo"; - } - - }, false, String.class); + (amqpMessage, channel1, message1, exception) -> { + ehCalled.set(true); + return "foo"; + }, false, String.class); listener.setMessageConverter(new MessageConverter() { @Override diff --git a/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt b/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt index c1746759e9..87fe6dba34 100644 --- a/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt +++ b/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt @@ -151,7 +151,7 @@ class EnableRabbitKotlinTests { val ehLatch = CountDownLatch(1) @Bean - fun eh() = RabbitListenerErrorHandler { _, _, _ -> + fun eh() = RabbitListenerErrorHandler { _, _, _, _ -> this.ehLatch.countDown() "error processed" } From 35e1cf8b464c3fd4d4439fc53ec5d59ca106fced Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 17 Jun 2024 10:12:13 -0400 Subject: [PATCH 476/737] Fix `dependabot.yml` for Gradle Develocity as DEV dep --- .github/dependabot.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ea7fd6cdba..5a8f11e0aa 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -18,7 +18,7 @@ updates: update-types: - patch patterns: - - com.gradle.enterprise + - com.gradle.* - com.github.spotbugs - io.spring.* - org.ajoberstar.grgit @@ -51,7 +51,7 @@ updates: update-types: - patch patterns: - - com.gradle.enterprise + - com.gradle.* - com.github.spotbugs - io.spring.* - org.ajoberstar.grgit From f81d86643254743602812d885b02cfda59d1a52a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 10:16:54 -0400 Subject: [PATCH 477/737] Bump the development-dependencies group across 1 directory with 2 updates (#2740) Bumps the development-dependencies group with 2 updates in the / directory: com.github.spotbugs and com.gradle.develocity. Updates `com.github.spotbugs` from 6.0.15 to 6.0.17 Updates `com.gradle.develocity` from 3.17.4 to 3.17.5 --- updated-dependencies: - dependency-name: com.github.spotbugs dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies - dependency-name: com.gradle.develocity dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- settings.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index aef0873dd3..bb5d3982f5 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ plugins { id 'io.spring.dependency-management' version '1.1.5' apply false id 'org.antora' version '1.0.0' id 'io.spring.antora.generate-antora-yml' version '0.0.1' - id 'com.github.spotbugs' version '6.0.15' + id 'com.github.spotbugs' version '6.0.17' } description = 'Spring AMQP' diff --git a/settings.gradle b/settings.gradle index e0c67ff13c..d5256c2078 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,5 @@ plugins { - id 'com.gradle.develocity' version '3.17.4' + id 'com.gradle.develocity' version '3.17.5' id 'io.spring.ge.conventions' version '0.0.17' } From 52b68ba9e81ef141a12570e9103dc29cceac37d2 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 17 Jun 2024 13:40:05 -0400 Subject: [PATCH 478/737] GH-2741: SMLC: Release consumer after its main loop Fixes: #2741 Apparently the consumer might be in the cancelled state, so `this.activeObjectCounter.release(this);` in the `BlockingQueueConsumer.nextMessage()` is not reachable. **Auto-cherry-pick to `3.1.x`** --- .../amqp/rabbit/listener/SimpleMessageListenerContainer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java index cfdb9cc5ab..5e4ae9ce22 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java @@ -1396,6 +1396,7 @@ public void run() { // NOSONAR - line count } } finally { + SimpleMessageListenerContainer.this.cancellationLock.release(this.consumer); if (getTransactionManager() != null) { ConsumerChannelRegistry.unRegisterConsumerChannel(); } From 887ea9f96c00658a211cc02e0f32615063b93d6a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 29 Jun 2024 02:17:30 +0000 Subject: [PATCH 479/737] Bump com.github.spotbugs in the development-dependencies group (#2745) Bumps the development-dependencies group with 1 update: com.github.spotbugs. Updates `com.github.spotbugs` from 6.0.17 to 6.0.18 --- updated-dependencies: - dependency-name: com.github.spotbugs dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index bb5d3982f5..3370c2cb80 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ plugins { id 'io.spring.dependency-management' version '1.1.5' apply false id 'org.antora' version '1.0.0' id 'io.spring.antora.generate-antora-yml' version '0.0.1' - id 'com.github.spotbugs' version '6.0.17' + id 'com.github.spotbugs' version '6.0.18' } description = 'Spring AMQP' From 9186b80be40484724160fa064480e872813a552b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 6 Jul 2024 02:45:02 +0000 Subject: [PATCH 480/737] Bump com.fasterxml.jackson:jackson-bom from 2.17.1 to 2.17.2 (#2748) Bumps [com.fasterxml.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) from 2.17.1 to 2.17.2. - [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-2.17.1...jackson-bom-2.17.2) --- updated-dependencies: - dependency-name: com.fasterxml.jackson:jackson-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 3370c2cb80..d574417d3d 100644 --- a/build.gradle +++ b/build.gradle @@ -51,7 +51,7 @@ ext { commonsPoolVersion = '2.12.0' hamcrestVersion = '2.2' hibernateValidationVersion = '8.0.1.Final' - jacksonBomVersion = '2.17.1' + jacksonBomVersion = '2.17.2' jaywayJsonPathVersion = '2.9.0' junit4Version = '4.13.2' junitJupiterVersion = '5.11.0-M2' From d22bb1f722e3fc5408f1ddfba1bf957c26c080a1 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 8 Jul 2024 10:19:44 -0400 Subject: [PATCH 481/737] Migrate to `io.spring.develocity.conventions` **Auto-cherry-pick to `3.1.x`** --- settings.gradle | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index d5256c2078..10e1e6d46e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,13 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } +} + plugins { id 'com.gradle.develocity' version '3.17.5' - id 'io.spring.ge.conventions' version '0.0.17' + id 'io.spring.develocity.conventions' version '0.0.19' } rootProject.name = 'spring-amqp-dist' From e16db98d1fe5ddc0a057d63b3f5d91c1d01bc882 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 9 Jul 2024 12:55:59 -0400 Subject: [PATCH 482/737] GH-2728: Add Consistent Hash Exchange support Fixes: #2728 --- .../amqp/core/BaseExchangeBuilder.java | 203 ++++++++++++++++++ .../amqp/core/ConsistentHashExchange.java | 97 +++++++++ .../amqp/core/ExchangeBuilder.java | 175 ++++----------- .../amqp/core/ExchangeTypes.java | 16 +- .../amqp/core/builder/BuilderTests.java | 23 +- .../modules/ROOT/pages/amqp/abstractions.adoc | 8 +- .../changes-in-3-1-since-3-0.adoc | 8 +- .../antora/modules/ROOT/pages/index.adoc | 6 +- .../antora/modules/ROOT/pages/whats-new.adoc | 7 +- 9 files changed, 389 insertions(+), 154 deletions(-) create mode 100644 spring-amqp/src/main/java/org/springframework/amqp/core/BaseExchangeBuilder.java create mode 100644 spring-amqp/src/main/java/org/springframework/amqp/core/ConsistentHashExchange.java diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/BaseExchangeBuilder.java b/spring-amqp/src/main/java/org/springframework/amqp/core/BaseExchangeBuilder.java new file mode 100644 index 0000000000..0e956e034c --- /dev/null +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/BaseExchangeBuilder.java @@ -0,0 +1,203 @@ +/* + * Copyright 2024 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.core; + +import java.util.Arrays; +import java.util.Map; + +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * An {@link AbstractBuilder} extension for generics support. + * + * @param the target class implementation type. + * + * @author Gary Russell + * @author Artem Bilan + * + * @since 3.2 + * + */ +public abstract class BaseExchangeBuilder> extends AbstractBuilder { + + protected final String name; + + protected final String type; + + protected boolean durable = true; + + protected boolean autoDelete; + + protected boolean internal; + + private boolean delayed; + + private boolean ignoreDeclarationExceptions; + + private boolean declare = true; + + private Object[] declaringAdmins; + + /** + * Construct an instance of the appropriate type. + * @param name the exchange name + * @param type the type name + * @since 1.6.7 + * @see ExchangeTypes + */ + public BaseExchangeBuilder(String name, String type) { + this.name = name; + this.type = type; + } + + + /** + * Set the auto delete flag. + * @return the builder. + */ + public B autoDelete() { + this.autoDelete = true; + return _this(); + } + + /** + * Set the durable flag. + * @param isDurable the durable flag (default true). + * @return the builder. + */ + public B durable(boolean isDurable) { + this.durable = isDurable; + return _this(); + } + + /** + * Add an argument. + * @param key the argument key. + * @param value the argument value. + * @return the builder. + */ + public B withArgument(String key, Object value) { + getOrCreateArguments().put(key, value); + return _this(); + } + + /** + * Add the arguments. + * @param arguments the arguments map. + * @return the builder. + */ + public B withArguments(Map arguments) { + this.getOrCreateArguments().putAll(arguments); + return _this(); + } + + public B alternate(String exchange) { + return withArgument("alternate-exchange", exchange); + } + + /** + * Set the internal flag. + * @return the builder. + */ + public B internal() { + this.internal = true; + return _this(); + } + + /** + * Set the delayed flag. + * @return the builder. + */ + public B delayed() { + this.delayed = true; + return _this(); + } + + /** + * Switch on ignore exceptions such as mismatched properties when declaring. + * @return the builder. + * @since 2.0 + */ + public B ignoreDeclarationExceptions() { + this.ignoreDeclarationExceptions = true; + return _this(); + } + + /** + * Switch to disable declaration of the exchange by any admin. + * @return the builder. + * @since 2.1 + */ + public B suppressDeclaration() { + this.declare = false; + return _this(); + } + + /** + * Admin instances, or admin bean names that should declare this exchange. + * @param admins the admins. + * @return the builder. + * @since 2.1 + */ + public B admins(Object... admins) { + Assert.notNull(admins, "'admins' cannot be null"); + Assert.noNullElements(admins, "'admins' can't have null elements"); + this.declaringAdmins = Arrays.copyOf(admins, admins.length); + return _this(); + } + + @SuppressWarnings("unchecked") + public T build() { + AbstractExchange exchange; + if (ExchangeTypes.DIRECT.equals(this.type)) { + exchange = new DirectExchange(this.name, this.durable, this.autoDelete, getArguments()); + } + else if (ExchangeTypes.TOPIC.equals(this.type)) { + exchange = new TopicExchange(this.name, this.durable, this.autoDelete, getArguments()); + } + else if (ExchangeTypes.FANOUT.equals(this.type)) { + exchange = new FanoutExchange(this.name, this.durable, this.autoDelete, getArguments()); + } + else if (ExchangeTypes.HEADERS.equals(this.type)) { + exchange = new HeadersExchange(this.name, this.durable, this.autoDelete, getArguments()); + } + else { + exchange = new CustomExchange(this.name, this.type, this.durable, this.autoDelete, getArguments()); + } + + return (T) configureExchange(exchange); + } + + + protected T configureExchange(T exchange) { + exchange.setInternal(this.internal); + exchange.setDelayed(this.delayed); + exchange.setIgnoreDeclarationExceptions(this.ignoreDeclarationExceptions); + exchange.setShouldDeclare(this.declare); + if (!ObjectUtils.isEmpty(this.declaringAdmins)) { + exchange.setAdminsThatShouldDeclare(this.declaringAdmins); + } + return exchange; + } + + @SuppressWarnings("unchecked") + protected final B _this() { + return (B) this; + } + +} diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/ConsistentHashExchange.java b/spring-amqp/src/main/java/org/springframework/amqp/core/ConsistentHashExchange.java new file mode 100644 index 0000000000..47eefd7a85 --- /dev/null +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/ConsistentHashExchange.java @@ -0,0 +1,97 @@ +/* + * Copyright 2024 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.core; + +import java.util.Map; + +import org.springframework.util.Assert; + +/** + * An {@link AbstractExchange} extension for Consistent Hash exchange type. + * + * @author Artem Bilan + * + * @since 3.2 + * + * @see AmqpAdmin + */ +public class ConsistentHashExchange extends AbstractExchange { + + /** + * Construct a new durable, non-auto-delete Exchange with the provided name. + * @param name the name of the exchange. + */ + public ConsistentHashExchange(String name) { + super(name); + } + + /** + * Construct a new Exchange, given a name, durability flag, auto-delete flag. + * @param name the name of the exchange. + * @param durable true if we are declaring a durable exchange (the exchange will + * survive a server restart) + * @param autoDelete true if the server should delete the exchange when it is no + * longer in use + */ + public ConsistentHashExchange(String name, boolean durable, boolean autoDelete) { + super(name, durable, autoDelete); + } + + /** + * Construct a new Exchange, given a name, durability flag, and auto-delete flag, and + * arguments. + * @param name the name of the exchange. + * @param durable true if we are declaring a durable exchange (the exchange will + * survive a server restart) + * @param autoDelete true if the server should delete the exchange when it is no + * longer in use + * @param arguments the arguments used to declare the exchange + */ + public ConsistentHashExchange(String name, boolean durable, boolean autoDelete, Map arguments) { + super(name, durable, autoDelete, arguments); + Assert.isTrue(!(arguments.containsKey("hash-header") && arguments.containsKey("hash-property")), + "The 'hash-header' and 'hash-property' are mutually exclusive."); + } + + /** + * Specify a header name from the message to hash. + * @param headerName the header name for hashing. + */ + public void setHashHeader(String headerName) { + Map arguments = getArguments(); + Assert.isTrue(!arguments.containsKey("hash-property"), + "The 'hash-header' and 'hash-property' are mutually exclusive."); + arguments.put("hash-header", headerName); + } + + /** + * Specify a property name from the message to hash. + * @param propertyName the property name for hashing. + */ + public void setHashProperty(String propertyName) { + Map arguments = getArguments(); + Assert.isTrue(!arguments.containsKey("hash-header"), + "The 'hash-header' and 'hash-property' are mutually exclusive."); + arguments.put("hash-property", propertyName); + } + + @Override + public String getType() { + return ExchangeTypes.CONSISTENT_HASH; + } + +} diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/ExchangeBuilder.java b/spring-amqp/src/main/java/org/springframework/amqp/core/ExchangeBuilder.java index af3405fade..273a9b84ca 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/ExchangeBuilder.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/ExchangeBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2024 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. @@ -16,12 +16,6 @@ package org.springframework.amqp.core; -import java.util.Arrays; -import java.util.Map; - -import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; - /** * Builder providing a fluent API for building {@link Exchange}s. * @@ -29,27 +23,8 @@ * @author Artem Bilan * * @since 1.6 - * */ -public final class ExchangeBuilder extends AbstractBuilder { - - private final String name; - - private final String type; - - private boolean durable = true; - - private boolean autoDelete; - - private boolean internal; - - private boolean delayed; - - private boolean ignoreDeclarationExceptions; - - private boolean declare = true; - - private Object[] declaringAdmins; +public class ExchangeBuilder extends BaseExchangeBuilder { /** * Construct an instance of the appropriate type. @@ -59,8 +34,7 @@ public final class ExchangeBuilder extends AbstractBuilder { * @see ExchangeTypes */ public ExchangeBuilder(String name, String type) { - this.name = name; - this.type = type; + super(name, type); } /** @@ -100,126 +74,49 @@ public static ExchangeBuilder headersExchange(String name) { } /** - * Set the auto delete flag. - * @return the builder. - */ - public ExchangeBuilder autoDelete() { - this.autoDelete = true; - return this; - } - - /** - * Set the durable flag. - * @param isDurable the durable flag (default true). - * @return the builder. - */ - public ExchangeBuilder durable(boolean isDurable) { - this.durable = isDurable; - return this; - } - - /** - * Add an argument. - * @param key the argument key. - * @param value the argument value. - * @return the builder. - */ - public ExchangeBuilder withArgument(String key, Object value) { - getOrCreateArguments().put(key, value); - return this; - } - - /** - * Add the arguments. - * @param arguments the arguments map. - * @return the builder. - */ - public ExchangeBuilder withArguments(Map arguments) { - this.getOrCreateArguments().putAll(arguments); - return this; - } - - public ExchangeBuilder alternate(String exchange) { - return withArgument("alternate-exchange", exchange); - } - - /** - * Set the internal flag. - * @return the builder. - */ - public ExchangeBuilder internal() { - this.internal = true; - return this; - } - - /** - * Set the delayed flag. - * @return the builder. - */ - public ExchangeBuilder delayed() { - this.delayed = true; - return this; - } - - /** - * Switch on ignore exceptions such as mismatched properties when declaring. - * @return the builder. - * @since 2.0 - */ - public ExchangeBuilder ignoreDeclarationExceptions() { - this.ignoreDeclarationExceptions = true; - return this; - } - - /** - * Switch to disable declaration of the exchange by any admin. + * Return an {@code x-consistent-hash} exchange builder. + * @param name the name. * @return the builder. - * @since 2.1 + * @since 3.2 */ - public ExchangeBuilder suppressDeclaration() { - this.declare = false; - return this; + public static ConsistentHashExchangeBuilder consistentHashExchange(String name) { + return new ConsistentHashExchangeBuilder(name); } /** - * Admin instances, or admin bean names that should declare this exchange. - * @param admins the admins. - * @return the builder. - * @since 2.1 + * An {@link ExchangeBuilder} extension for the {@link ConsistentHashExchange}. + * + * @since 3.2 */ - public ExchangeBuilder admins(Object... admins) { - Assert.notNull(admins, "'admins' cannot be null"); - Assert.noNullElements(admins, "'admins' can't have null elements"); - this.declaringAdmins = Arrays.copyOf(admins, admins.length); - return this; - } - - @SuppressWarnings("unchecked") - public T build() { - AbstractExchange exchange; - if (ExchangeTypes.DIRECT.equals(this.type)) { - exchange = new DirectExchange(this.name, this.durable, this.autoDelete, getArguments()); - } - else if (ExchangeTypes.TOPIC.equals(this.type)) { - exchange = new TopicExchange(this.name, this.durable, this.autoDelete, getArguments()); + public static final class ConsistentHashExchangeBuilder extends BaseExchangeBuilder { + + /** + * Construct an instance of the builder for {@link ConsistentHashExchange}. + * + * @param name the exchange name + * @see ExchangeTypes + */ + public ConsistentHashExchangeBuilder(String name) { + super(name, ExchangeTypes.CONSISTENT_HASH); } - else if (ExchangeTypes.FANOUT.equals(this.type)) { - exchange = new FanoutExchange(this.name, this.durable, this.autoDelete, getArguments()); - } - else if (ExchangeTypes.HEADERS.equals(this.type)) { - exchange = new HeadersExchange(this.name, this.durable, this.autoDelete, getArguments()); + + public ConsistentHashExchangeBuilder hashHeader(String headerName) { + withArgument("hash-header", headerName); + return this; } - else { - exchange = new CustomExchange(this.name, this.type, this.durable, this.autoDelete, getArguments()); + + public ConsistentHashExchangeBuilder hashProperty(String propertyName) { + withArgument("hash-property", propertyName); + return this; } - exchange.setInternal(this.internal); - exchange.setDelayed(this.delayed); - exchange.setIgnoreDeclarationExceptions(this.ignoreDeclarationExceptions); - exchange.setShouldDeclare(this.declare); - if (!ObjectUtils.isEmpty(this.declaringAdmins)) { - exchange.setAdminsThatShouldDeclare(this.declaringAdmins); + + @Override + @SuppressWarnings("unchecked") + public ConsistentHashExchange build() { + return configureExchange( + new ConsistentHashExchange(this.name, this.durable, this.autoDelete, getArguments())); } - return (T) exchange; + } } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/ExchangeTypes.java b/spring-amqp/src/main/java/org/springframework/amqp/core/ExchangeTypes.java index 291d940fe9..472d9ece8b 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/ExchangeTypes.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/ExchangeTypes.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -21,8 +21,9 @@ * * @author Mark Fisher * @author Gary Russell + * @author Artem Bilan */ -public abstract class ExchangeTypes { +public final class ExchangeTypes { /** * Direct exchange. @@ -44,9 +45,20 @@ public abstract class ExchangeTypes { */ public static final String HEADERS = "headers"; + /** + * Consistent Hash exchange. + * @since 3.2 + */ + public static final String CONSISTENT_HASH = "x-consistent-hash"; + /** * System exchange. + * @deprecated with no replacement (for removal): there is no such an exchange type in AMQP. */ + @Deprecated(since = "3.2", forRemoval = true) public static final String SYSTEM = "system"; + private ExchangeTypes() { + } + } diff --git a/spring-amqp/src/test/java/org/springframework/amqp/core/builder/BuilderTests.java b/spring-amqp/src/test/java/org/springframework/amqp/core/builder/BuilderTests.java index fff6e8a996..7bf19eec29 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/core/builder/BuilderTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/core/builder/BuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2024 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. @@ -17,9 +17,11 @@ package org.springframework.amqp.core.builder; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import org.junit.jupiter.api.Test; +import org.springframework.amqp.core.ConsistentHashExchange; import org.springframework.amqp.core.DirectExchange; import org.springframework.amqp.core.Exchange; import org.springframework.amqp.core.ExchangeBuilder; @@ -31,8 +33,8 @@ /** * @author Gary Russell + * @author Artem Bilan * @since 1.6 - * */ public class BuilderTests { @@ -89,6 +91,23 @@ public void testExchangeBuilder() { assertThat(exchange.isDurable()).isTrue(); assertThat(exchange.isInternal()).isFalse(); assertThat(exchange.isDelayed()).isFalse(); + + exchange = ExchangeBuilder.consistentHashExchange("foo") + .ignoreDeclarationExceptions() + .hashHeader("my_header") + .build(); + + assertThat(exchange).isInstanceOf(ConsistentHashExchange.class); + assertThat((String) exchange.getArguments().get("hash-header")).isEqualTo("my_header"); + + assertThatIllegalArgumentException() + .isThrownBy(() -> + ExchangeBuilder.consistentHashExchange("wrong_exchange") + .hashHeader("my_header") + .hashProperty("my_property") + .build()) + .withMessage("The 'hash-header' and 'hash-property' are mutually exclusive."); + } } diff --git a/src/reference/antora/modules/ROOT/pages/amqp/abstractions.adoc b/src/reference/antora/modules/ROOT/pages/amqp/abstractions.adoc index 192c5c7f13..ca252a56c1 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/abstractions.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/abstractions.adoc @@ -87,7 +87,13 @@ The behavior varies across these `Exchange` types in terms of how they handle bi For example, a `Direct` exchange lets a queue be bound by a fixed routing key (often the queue's name). A `Topic` exchange supports bindings with routing patterns that may include the '*' and '#' wildcards for 'exactly-one' and 'zero-or-more', respectively. The `Fanout` exchange publishes to all queues that are bound to it without taking any routing key into consideration. -For much more information about these and the other Exchange types, see xref:index.adoc#resources[Other Resources]. +For much more information about these and the other Exchange types, see https://www.rabbitmq.com/tutorials/amqp-concepts#exchanges[AMQP Exchanges]. + +Starting with version 3.2, the `ConsistentHashExchange` type has been introduced for convenience during application configuration phase. +It provided options like `x-consistent-hash` for an exchange type. +Allows to configure `hash-header` or `hash-property` exchange definition argument. +The respective RabbitMQ `rabbitmq_consistent_hash_exchange` plugin has to be enabled on the broker. +More information about the purpose, logic and behavior of the Consistent Hash Exchange are in the official RabbitMQ https://github.com/rabbitmq/rabbitmq-server/tree/main/deps/rabbitmq_consistent_hash_exchange[documentation]. NOTE: The AMQP specification also requires that any broker provide a "`default`" direct exchange that has no name. All queues that are declared are bound to that default `Exchange` with their names as routing keys. diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-3-1-since-3-0.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-3-1-since-3-0.adoc index 7c8327fcd2..171b94d77c 100644 --- a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-3-1-since-3-0.adoc +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-3-1-since-3-0.adoc @@ -1,13 +1,13 @@ [[changes-in-3-1-since-3-0]] -== Changes in 3.1 Since 3.0 += Changes in 3.1 Since 3.0 [[java-17-spring-framework-6-1]] -=== Java 17, Spring Framework 6.1 +== Java 17, Spring Framework 6.1 This version requires Spring Framework 6.1 and Java 17. [[x31-exc]] -=== Exclusive Consumer Logging +== Exclusive Consumer Logging Log messages reporting access refusal due to exclusive consumers are now logged at DEBUG level by default. It remains possible to configure your own logging behavior by setting the `exclusiveConsumerExceptionLogger` and `closeExceptionLogger` properties on the listener container and connection factory respectively. @@ -16,7 +16,7 @@ A new method `logRestart()` has been added to the `ConditionalExceptionLogger` t See xref:amqp/receiving-messages/consumer-events.adoc[Consumer Events] and xref:amqp/connections.adoc#channel-close-logging[Logging Channel Close Events] for more information. [[x31-conn-backoff]] -=== Connections Enhancement +== Connections Enhancement Connection Factory supported backoff policy when creating connection channel. See xref:amqp/connections.adoc[Choosing a Connection Factory] for more information. diff --git a/src/reference/antora/modules/ROOT/pages/index.adoc b/src/reference/antora/modules/ROOT/pages/index.adoc index ab6ddca384..1648e79ad4 100644 --- a/src/reference/antora/modules/ROOT/pages/index.adoc +++ b/src/reference/antora/modules/ROOT/pages/index.adoc @@ -11,8 +11,4 @@ We provide a "`template`" as a high-level abstraction for sending and receiving We also provide support for message-driven POJOs. These libraries facilitate management of AMQP resources while promoting the use of dependency injection and declarative configuration. In all of these cases, you can see similarities to the JMS support in the Spring Framework. -For other project-related information, visit the Spring AMQP project https://projects.spring.io/spring-amqp/[homepage]. - -(C) 2010 - 2023 - -Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically. \ No newline at end of file +For other project-related information, visit the Spring AMQP project https://projects.spring.io/spring-amqp/[homepage]. \ No newline at end of file diff --git a/src/reference/antora/modules/ROOT/pages/whats-new.adoc b/src/reference/antora/modules/ROOT/pages/whats-new.adoc index 5c8c40f205..3258c28f44 100644 --- a/src/reference/antora/modules/ROOT/pages/whats-new.adoc +++ b/src/reference/antora/modules/ROOT/pages/whats-new.adoc @@ -8,4 +8,9 @@ [[spring-framework-6-2]] === Spring Framework 6.1 -This version requires Spring Framework 6.2. \ No newline at end of file +This version requires Spring Framework 6.2. + +[[x32-consistent-hash-exchange]] +=== Consistent Hash Exchange + +The convenient `ConsistentHashExchange` and respective `ExchangeBuilder.consistentHashExchange()` API has been introduced. \ No newline at end of file From 682aefe48510e62638541d5b1f370919a33783ce Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 11 Jul 2024 14:46:20 -0400 Subject: [PATCH 483/737] Migrate release notification to Google Chat **Cherry-pick to `3.1.x`** --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f7e832bdef..8ccfe1ec32 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,4 +25,4 @@ jobs: OSSRH_STAGING_PROFILE_NAME: ${{ secrets.OSSRH_STAGING_PROFILE_NAME }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} - SPRING_RELEASE_SLACK_WEBHOOK_URL: ${{ secrets.SPRING_RELEASE_SLACK_WEBHOOK_URL }} \ No newline at end of file + SPRING_RELEASE_CHAT_WEBHOOK_URL: ${{ secrets.SPRING_RELEASE_GCHAT_WEBHOOK_URL }} \ No newline at end of file From 6bef08324adeb8fd30e19eae5e34ce2ec7860cdc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Jul 2024 02:26:18 +0000 Subject: [PATCH 484/737] Bump org.assertj:assertj-core from 3.26.0 to 3.26.3 (#2750) Bumps [org.assertj:assertj-core](https://github.com/assertj/assertj) from 3.26.0 to 3.26.3. - [Release notes](https://github.com/assertj/assertj/releases) - [Commits](https://github.com/assertj/assertj/compare/assertj-build-3.26.0...assertj-build-3.26.3) --- updated-dependencies: - dependency-name: org.assertj:assertj-core dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d574417d3d..68b7786b6d 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ ext { .filter { f -> f.name.endsWith('.java') || f.name.endsWith('.kt') } } - assertjVersion = '3.26.0' + assertjVersion = '3.26.3' assertkVersion = '0.28.1' awaitilityVersion = '4.2.1' commonsCompressVersion = '1.26.2' From 43a22ceaf303063c688d31b1ba09a07de0147d79 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Jul 2024 02:26:34 +0000 Subject: [PATCH 485/737] Bump the development-dependencies group with 3 updates (#2749) Bumps the development-dependencies group with 3 updates: [io.micrometer:micrometer-docs-generator](https://github.com/micrometer-metrics/micrometer-docs-generator), io.spring.dependency-management and com.github.spotbugs. Updates `io.micrometer:micrometer-docs-generator` from 1.0.2 to 1.0.3 - [Release notes](https://github.com/micrometer-metrics/micrometer-docs-generator/releases) - [Commits](https://github.com/micrometer-metrics/micrometer-docs-generator/compare/v1.0.2...v1.0.3) Updates `io.spring.dependency-management` from 1.1.5 to 1.1.6 Updates `com.github.spotbugs` from 6.0.18 to 6.0.19 --- updated-dependencies: - dependency-name: io.micrometer:micrometer-docs-generator dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies - dependency-name: io.spring.dependency-management dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies - dependency-name: com.github.spotbugs dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 68b7786b6d..1e8f998857 100644 --- a/build.gradle +++ b/build.gradle @@ -20,10 +20,10 @@ plugins { id 'idea' id 'org.ajoberstar.grgit' version '5.2.2' id 'io.spring.nohttp' version '0.0.11' - id 'io.spring.dependency-management' version '1.1.5' apply false + id 'io.spring.dependency-management' version '1.1.6' apply false id 'org.antora' version '1.0.0' id 'io.spring.antora.generate-antora-yml' version '0.0.1' - id 'com.github.spotbugs' version '6.0.18' + id 'com.github.spotbugs' version '6.0.19' } description = 'Spring AMQP' @@ -59,7 +59,7 @@ ext { log4jVersion = '2.23.1' logbackVersion = '1.5.6' lz4Version = '1.8.0' - micrometerDocsVersion = '1.0.2' + micrometerDocsVersion = '1.0.3' micrometerVersion = '1.14.0-SNAPSHOT' micrometerTracingVersion = '1.4.0-SNAPSHOT' mockitoVersion = '5.12.0' From 2c0f1c2d263c70aece48eee2802dc138852cb6c4 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 15 Jul 2024 12:01:04 -0400 Subject: [PATCH 486/737] Use latest Milestones for deps; prepare for release --- build.gradle | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 1e8f998857..e3d9ff85a6 100644 --- a/build.gradle +++ b/build.gradle @@ -60,16 +60,16 @@ ext { logbackVersion = '1.5.6' lz4Version = '1.8.0' micrometerDocsVersion = '1.0.3' - micrometerVersion = '1.14.0-SNAPSHOT' - micrometerTracingVersion = '1.4.0-SNAPSHOT' + micrometerVersion = '1.14.0-M1' + micrometerTracingVersion = '1.4.0-M1' mockitoVersion = '5.12.0' rabbitmqStreamVersion = '0.15.0' rabbitmqVersion = '5.21.0' - reactorVersion = '2024.0.0-SNAPSHOT' + reactorVersion = '2024.0.0-M4' snappyVersion = '1.1.10.5' - springDataVersion = '2024.1.0-SNAPSHOT' + springDataVersion = '2024.0.3' springRetryVersion = '2.0.6' - springVersion = '6.2.0-SNAPSHOT' + springVersion = '6.2.0-M5' testcontainersVersion = '1.19.8' zstdJniVersion = '1.5.6-3' From 2f10f8d0c2af3b4fc32e4f30c3f0d5342c11c4fc Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 15 Jul 2024 12:03:53 -0400 Subject: [PATCH 487/737] Fix Spring Data version to `2024.0.2` --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e3d9ff85a6..fdf07e7f06 100644 --- a/build.gradle +++ b/build.gradle @@ -67,7 +67,7 @@ ext { rabbitmqVersion = '5.21.0' reactorVersion = '2024.0.0-M4' snappyVersion = '1.1.10.5' - springDataVersion = '2024.0.3' + springDataVersion = '2024.0.2' springRetryVersion = '2.0.6' springVersion = '6.2.0-M5' testcontainersVersion = '1.19.8' From 437d6b1e9cb500be6e7686dbf2162be3cab6b638 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 15 Jul 2024 16:10:24 +0000 Subject: [PATCH 488/737] [artifactory-release] Release version 3.2.0-M1 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 6bc0422ab1..af3cd8028c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.2.0-SNAPSHOT +version=3.2.0-M1 org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true From ff38c6eef6baba2af5ffc653b27113df7ccefa1e Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 15 Jul 2024 16:10:26 +0000 Subject: [PATCH 489/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index af3cd8028c..6bc0422ab1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.2.0-M1 +version=3.2.0-SNAPSHOT org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true From 9c660adb296f2723acff7c4c633e628746dbaa7f Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 15 Jul 2024 12:22:58 -0400 Subject: [PATCH 490/737] Fix verify-staged-artifacts against Spring Integration The Maven-based verification against Samples repository suffers from the limitation on the `jf mvnc` and our `libs-release-staging` virtual repository not able to get access to Milestones --- .github/workflows/verify-staged-artifacts.yml | 57 ++++++++++--------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/.github/workflows/verify-staged-artifacts.yml b/.github/workflows/verify-staged-artifacts.yml index 5f9b32bda5..bee9abc943 100644 --- a/.github/workflows/verify-staged-artifacts.yml +++ b/.github/workflows/verify-staged-artifacts.yml @@ -8,8 +8,16 @@ on: required: true type: string + +env: + GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} + GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} + GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} + ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} + jobs: - verify-staged-with-samples: + verify-staged-with-spring-integration: runs-on: ubuntu-latest steps: @@ -20,36 +28,31 @@ jobs: ports: '5672:5672 15672:15672 5552:5552' plugins: rabbitmq_stream,rabbitmq_management - - name: Checkout Samples Repo + - name: Checkout Spring Integration Repo uses: actions/checkout@v4 with: - repository: spring-projects/spring-amqp-samples - ref: ${{ github.ref_name }} + repository: spring-projects/spring-integration show-progress: false - - name: Set up JDK - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: 17 - cache: 'maven' - - - uses: jfrog/setup-jfrog-cli@v4 - env: - JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} - - name: Configure JFrog Cli - run: jf mvnc --repo-resolve-releases=libs-release-staging --repo-resolve-snapshots=snapshot + - name: Set up Gradle + uses: spring-io/spring-gradle-build-action@v2 - - name: Verify samples against staged release + - name: Prepare Spring Integration project against Staging run: | - mvn versions:set -DnewVersion=${{ inputs.releaseVersion }} -DgenerateBackupPoms=false -DprocessAllModules=true -B -ntp - jf mvn verify -B -ntp - - - name: Capture Test Results - if: failure() - uses: actions/upload-artifact@v4 - with: - name: test-results - path: '**/target/surefire-reports/**/*.*' - retention-days: 1 \ No newline at end of file + printf "allprojects { + repositories { + maven { + url 'https://repo.spring.io/libs-staging-local' + credentials { + username = '$ARTIFACTORY_USERNAME' + password = '$ARTIFACTORY_PASSWORD' + } + } + } + }" > staging-repo-init.gradle + + sed -i "1,/springAmqpVersion.*/s/springAmqpVersion.*/springAmqpVersion='${{ inputs.releaseVersion }}'/" build.gradle + + - name: Verify Spring Integration AMQP module against staged release + run: gradle :spring-integration-amqp:check --init-script staging-repo-init.gradle \ No newline at end of file From d88f2fbd075cb22fc49246227abe5ef080bedd7a Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 15 Jul 2024 12:47:58 -0400 Subject: [PATCH 491/737] Add tentative `call-finalize.yml` --- .github/workflows/call-finalize.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/workflows/call-finalize.yml diff --git a/.github/workflows/call-finalize.yml b/.github/workflows/call-finalize.yml new file mode 100644 index 0000000000..253c12692c --- /dev/null +++ b/.github/workflows/call-finalize.yml @@ -0,0 +1,15 @@ +on: + workflow_dispatch: + inputs: + milestone: + description: 'Release version like 5.0.0-M1, 5.1.0-RC1, 5.2.0 etc.' + required: true + +jobs: + finalize: + uses: spring-io/spring-github-workflows/.github/workflows/spring-finalize-release.yml@main + with: + milestone: ${{ inputs.milestone }} + secrets: + GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} + SPRING_RELEASE_CHAT_WEBHOOK_URL: ${{ secrets.SPRING_RELEASE_GCHAT_WEBHOOK_URL }} From 686665ac43bf91a9ff532b9d644a5bb506296cc7 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 15 Jul 2024 12:50:25 -0400 Subject: [PATCH 492/737] Revert "Add tentative `call-finalize.yml`" This reverts commit d88f2fbd075cb22fc49246227abe5ef080bedd7a. --- .github/workflows/call-finalize.yml | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 .github/workflows/call-finalize.yml diff --git a/.github/workflows/call-finalize.yml b/.github/workflows/call-finalize.yml deleted file mode 100644 index 253c12692c..0000000000 --- a/.github/workflows/call-finalize.yml +++ /dev/null @@ -1,15 +0,0 @@ -on: - workflow_dispatch: - inputs: - milestone: - description: 'Release version like 5.0.0-M1, 5.1.0-RC1, 5.2.0 etc.' - required: true - -jobs: - finalize: - uses: spring-io/spring-github-workflows/.github/workflows/spring-finalize-release.yml@main - with: - milestone: ${{ inputs.milestone }} - secrets: - GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} - SPRING_RELEASE_CHAT_WEBHOOK_URL: ${{ secrets.SPRING_RELEASE_GCHAT_WEBHOOK_URL }} From 3130acf54b6f6d12384d30f6869de87e6d7cd1c7 Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Wed, 17 Jul 2024 00:39:16 +0700 Subject: [PATCH 493/737] Fix Docs: escape asterisk --- src/reference/antora/modules/ROOT/pages/amqp/abstractions.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reference/antora/modules/ROOT/pages/amqp/abstractions.adoc b/src/reference/antora/modules/ROOT/pages/amqp/abstractions.adoc index ca252a56c1..b86178a244 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/abstractions.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/abstractions.adoc @@ -53,7 +53,7 @@ IMPORTANT: Starting with versions `1.5.7`, `1.6.11`, `1.7.4`, and `2.0.0`, if a This is to prevent unsafe deserialization. By default, only `java.util` and `java.lang` classes are deserialized. To revert to the previous behavior, you can add allowable class/package patterns by invoking `Message.addAllowedListPatterns(...)`. -A simple `*` wildcard is supported, for example `com.something.*, *.MyClass`. +A simple `\*` wildcard is supported, for example `com.something.*, *.MyClass`. Bodies that cannot be deserialized are represented by `byte[]` in log messages. [[exchange]] From 7886dd10724cbf87cc80b59128577537557b4622 Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Wed, 17 Jul 2024 23:06:18 +0700 Subject: [PATCH 494/737] Fix links in docs * Remove `message-converter-changes-1.adoc` as a duplication of the `message-converter-changes.adoc` --- src/reference/antora/modules/ROOT/nav.adoc | 1 - .../modules/ROOT/pages/amqp/broker-configuration.adoc | 2 +- .../amqp/receiving-messages/micrometer-observation.adoc | 2 +- .../antora/modules/ROOT/pages/amqp/transactions.adoc | 2 +- .../previous-whats-new/changes-in-1-6-since-1-5.adoc | 2 +- .../previous-whats-new/message-converter-changes-1.adoc | 7 ------- .../previous-whats-new/message-converter-changes.adoc | 2 +- src/reference/antora/modules/ROOT/pages/sample-apps.adoc | 2 +- src/reference/antora/modules/ROOT/pages/stream.adoc | 2 +- 9 files changed, 7 insertions(+), 15 deletions(-) delete mode 100644 src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/message-converter-changes-1.adoc diff --git a/src/reference/antora/modules/ROOT/nav.adoc b/src/reference/antora/modules/ROOT/nav.adoc index 40dd5a27e1..a947118935 100644 --- a/src/reference/antora/modules/ROOT/nav.adoc +++ b/src/reference/antora/modules/ROOT/nav.adoc @@ -72,7 +72,6 @@ *** xref:appendix/previous-whats-new/changes-in-3-0-since-2-4.adoc[] *** xref:appendix/previous-whats-new/changes-in-2-4-since-2-3.adoc[] *** xref:appendix/previous-whats-new/message-converter-changes.adoc[] -*** xref:appendix/previous-whats-new/message-converter-changes-1.adoc[] *** xref:appendix/previous-whats-new/stream-support-changes.adoc[] *** xref:appendix/previous-whats-new/changes-in-2-3-since-2-2.adoc[] *** xref:appendix/previous-whats-new/changes-in-2-2-since-2-1.adoc[] diff --git a/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc b/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc index bd0160f618..c983716a37 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc @@ -359,7 +359,7 @@ public Exchange exchange() { } ---- -See the Javadoc for https://docs.spring.io/spring-amqp/docs/current/api/org/springframework/amqp/core/QueueBuilder.html[`org.springframework.amqp.core.QueueBuilder`] and https://docs.spring.io/spring-amqp/docs/latest-ga/api/org/springframework/amqp/core/ExchangeBuilder.html[`org.springframework.amqp.core.ExchangeBuilder`] for more information. +See the Javadoc for https://docs.spring.io/spring-amqp/docs/current/api/org/springframework/amqp/core/QueueBuilder.html[`org.springframework.amqp.core.QueueBuilder`] and https://docs.spring.io/spring-amqp/docs/current/api/org/springframework/amqp/core/ExchangeBuilder.html[`org.springframework.amqp.core.ExchangeBuilder`] for more information. Starting with version 2.0, the `ExchangeBuilder` now creates durable exchanges by default, to be consistent with the simple constructors on the individual `AbstractExchange` classes. To make a non-durable exchange with the builder, use `.durable(false)` before invoking `.build()`. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer-observation.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer-observation.adoc index d4e53cfcf8..7d30d09065 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer-observation.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer-observation.adoc @@ -7,7 +7,7 @@ Using Micrometer for observation is now supported, since version 3.0, for the `R Set `observationEnabled` on each component to enable observation; this will disable xref:amqp/receiving-messages/micrometer.adoc[Micrometer Timers] because the timers will now be managed with each observation. When using annotated listeners, set `observationEnabled` on the container factory. -Refer to https://micrometer.io/docs/tracing[Micrometer Tracing] for more information. +Refer to https://docs.micrometer.io/tracing/reference/[Micrometer Tracing] for more information. To add tags to timers/traces, configure a custom `RabbitTemplateObservationConvention` or `RabbitListenerObservationConvention` to the template or listener container, respectively. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/transactions.adoc b/src/reference/antora/modules/ROOT/pages/amqp/transactions.adoc index fad761b9f0..c007b02199 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/transactions.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/transactions.adoc @@ -116,7 +116,7 @@ See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration] [[using-rabbittransactionmanager]] == Using `RabbitTransactionManager` -The https://docs.spring.io/spring-amqp/docs/latest_ga/api/org/springframework/amqp/rabbit/transaction/RabbitTransactionManager.html[RabbitTransactionManager] is an alternative to executing Rabbit operations within, and synchronized with, external transactions. +The https://docs.spring.io/spring-amqp/docs/current/api/org/springframework/amqp/rabbit/transaction/RabbitTransactionManager.html[RabbitTransactionManager] is an alternative to executing Rabbit operations within, and synchronized with, external transactions. This transaction manager is an implementation of the https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/transaction/PlatformTransactionManager.html[`PlatformTransactionManager`] interface and should be used with a single Rabbit `ConnectionFactory`. IMPORTANT: This strategy is not able to provide XA transactions -- for example, in order to share transactions between messaging and database access. diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-6-since-1-5.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-6-since-1-5.adoc index 237a3053bc..02d8037f46 100644 --- a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-6-since-1-5.adoc +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-6-since-1-5.adoc @@ -242,7 +242,7 @@ See <> for more information. Improvements to the JSON message converter now allow the consumption of messages that do not have type information in message headers. -See xref:amqp/receiving-messages/async-annotation-driven/conversion.adoc[Message Conversion for Annotated Methods] and <> for more information. +See xref:amqp/receiving-messages/async-annotation-driven/conversion.adoc[Message Conversion for Annotated Methods] and xref:amqp/message-converters.adoc#json-message-converter[`Jackson2JsonMessageConverter`] for more information. [[logging-appenders]] == Logging Appenders diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/message-converter-changes-1.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/message-converter-changes-1.adoc deleted file mode 100644 index a342746ba4..0000000000 --- a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/message-converter-changes-1.adoc +++ /dev/null @@ -1,7 +0,0 @@ -[[message-converter-changes]] -= Message Converter Changes -:page-section-summary-toc: 1 - -The `Jackson2JsonMessageConverter` can now determine the charset from the `contentEncoding` header. -See <> for more information. - diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/message-converter-changes.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/message-converter-changes.adoc index a342746ba4..693a71cf40 100644 --- a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/message-converter-changes.adoc +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/message-converter-changes.adoc @@ -3,5 +3,5 @@ :page-section-summary-toc: 1 The `Jackson2JsonMessageConverter` can now determine the charset from the `contentEncoding` header. -See <> for more information. +See xref:amqp/message-converters.adoc#json-message-converter[`Jackson2JsonMessageConverter`] for more information. diff --git a/src/reference/antora/modules/ROOT/pages/sample-apps.adoc b/src/reference/antora/modules/ROOT/pages/sample-apps.adoc index 188a097b3c..bf983d5edc 100644 --- a/src/reference/antora/modules/ROOT/pages/sample-apps.adoc +++ b/src/reference/antora/modules/ROOT/pages/sample-apps.adoc @@ -351,4 +351,4 @@ Spring applications, when sending JSON, set the `__TypeId__` header to the fully The `spring-rabbit-json` sample explores several techniques to convert the JSON from a non-Spring application. -See also <> as well as the https://docs.spring.io/spring-amqp/docs/current/api/index.html?org/springframework/amqp/support/converter/DefaultClassMapper.html[Javadoc for the `DefaultClassMapper`]. +See also xref:amqp/message-converters.adoc#json-message-converter[`Jackson2JsonMessageConverter`] as well as the https://docs.spring.io/spring-amqp/docs/current/api/index.html?org/springframework/amqp/support/converter/DefaultClassMapper.html[Javadoc for the `DefaultClassMapper`]. diff --git a/src/reference/antora/modules/ROOT/pages/stream.adoc b/src/reference/antora/modules/ROOT/pages/stream.adoc index 52cbc6dc18..4994354fca 100644 --- a/src/reference/antora/modules/ROOT/pages/stream.adoc +++ b/src/reference/antora/modules/ROOT/pages/stream.adoc @@ -298,7 +298,7 @@ The container now also supports Micrometer timers (when observation is not enabl Set `observationEnabled` on each component to enable observation; this will disable xref:amqp/receiving-messages/micrometer.adoc[Micrometer Timers] because the timers will now be managed with each observation. When using annotated listeners, set `observationEnabled` on the container factory. -Refer to https://micrometer.io/docs/tracing[Micrometer Tracing] for more information. +Refer to https://docs.micrometer.io/tracing/reference/[Micrometer Tracing] for more information. To add tags to timers/traces, configure a custom `RabbitStreamTemplateObservationConvention` or `RabbitStreamListenerObservationConvention` to the template or listener container, respectively. From 9c204a75ab3922684615c9524d03c296ba941220 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 18 Jul 2024 16:16:45 -0400 Subject: [PATCH 495/737] Upgrade to reusable `spring-io/spring-github-workflows@v3` Apparently Dependabot can upgrade reusable GHA workflows as well. So less burden on support and less stress from changes in those reusable workflows in `main` **Auto-cherry-pick to `3.1.x`** --- .github/workflows/auto-cherry-pick.yml | 2 +- .github/workflows/backport-issue.yml | 2 +- .github/workflows/ci-snapshot.yml | 2 +- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/merge-dependabot-pr.yml | 2 +- .github/workflows/pr-build.yml | 2 +- .github/workflows/release.yml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/auto-cherry-pick.yml b/.github/workflows/auto-cherry-pick.yml index 4a9c4479ef..ad08e22428 100644 --- a/.github/workflows/auto-cherry-pick.yml +++ b/.github/workflows/auto-cherry-pick.yml @@ -8,6 +8,6 @@ on: jobs: cherry-pick-commit: - uses: spring-io/spring-github-workflows/.github/workflows/spring-cherry-pick.yml@main + uses: spring-io/spring-github-workflows/.github/workflows/spring-cherry-pick.yml@v3 secrets: GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/backport-issue.yml b/.github/workflows/backport-issue.yml index ae3ea05130..68de1977f1 100644 --- a/.github/workflows/backport-issue.yml +++ b/.github/workflows/backport-issue.yml @@ -7,6 +7,6 @@ on: jobs: backport-issue: - uses: spring-io/spring-github-workflows/.github/workflows/spring-backport-issue.yml@main + uses: spring-io/spring-github-workflows/.github/workflows/spring-backport-issue.yml@v3 secrets: GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/ci-snapshot.yml b/.github/workflows/ci-snapshot.yml index a94396f772..c57e8778e7 100644 --- a/.github/workflows/ci-snapshot.yml +++ b/.github/workflows/ci-snapshot.yml @@ -17,7 +17,7 @@ concurrency: jobs: build-snapshot: - uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-snapshot.yml@main + uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-snapshot.yml@v3 with: gradleTasks: ${{ github.event_name == 'schedule' && '--rerun-tasks' || '' }} secrets: diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 1771c58265..5726382e2f 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -16,4 +16,4 @@ permissions: jobs: dispatch-docs-build: if: github.repository_owner == 'spring-projects' - uses: spring-io/spring-github-workflows/.github/workflows/spring-dispatch-docs-build.yml@main + uses: spring-io/spring-github-workflows/.github/workflows/spring-dispatch-docs-build.yml@v3 diff --git a/.github/workflows/merge-dependabot-pr.yml b/.github/workflows/merge-dependabot-pr.yml index 743781a0dc..42d8329d59 100644 --- a/.github/workflows/merge-dependabot-pr.yml +++ b/.github/workflows/merge-dependabot-pr.yml @@ -12,6 +12,6 @@ jobs: merge-dependabot-pr: permissions: write-all - uses: spring-io/spring-github-workflows/.github/workflows/spring-merge-dependabot-pr.yml@main + uses: spring-io/spring-github-workflows/.github/workflows/spring-merge-dependabot-pr.yml@v3 with: mergeArguments: --auto --squash \ No newline at end of file diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index cefa6bf31a..b03b5bf93d 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -8,4 +8,4 @@ on: jobs: build-pull-request: - uses: spring-io/spring-github-workflows/.github/workflows/spring-gradle-pull-request-build.yml@main + uses: spring-io/spring-github-workflows/.github/workflows/spring-gradle-pull-request-build.yml@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8ccfe1ec32..3400ec5ba1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: contents: write issues: write - uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-release.yml@main + uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-release.yml@v3 secrets: GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} GRADLE_ENTERPRISE_SECRET_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} From c97d021ecb1d1e97e914e5bab1a06bd994597de1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jul 2024 22:15:42 -0400 Subject: [PATCH 496/737] Bump org.springframework:spring-framework-bom (#2761) Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.2.0-M5 to 6.2.0-SNAPSHOT. - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/commits) --- updated-dependencies: - dependency-name: org.springframework:spring-framework-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index fdf07e7f06..e39f94b10e 100644 --- a/build.gradle +++ b/build.gradle @@ -69,7 +69,7 @@ ext { snappyVersion = '1.1.10.5' springDataVersion = '2024.0.2' springRetryVersion = '2.0.6' - springVersion = '6.2.0-M5' + springVersion = '6.2.0-SNAPSHOT' testcontainersVersion = '1.19.8' zstdJniVersion = '1.5.6-3' From 0adefb7242d74304917d51cfc24fe4bfa669dcb4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jul 2024 22:16:04 -0400 Subject: [PATCH 497/737] Bump io.micrometer:micrometer-tracing-bom (#2762) Bumps [io.micrometer:micrometer-tracing-bom](https://github.com/micrometer-metrics/tracing) from 1.4.0-M1 to 1.4.0-SNAPSHOT. - [Release notes](https://github.com/micrometer-metrics/tracing/releases) - [Commits](https://github.com/micrometer-metrics/tracing/commits) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-tracing-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e39f94b10e..ca9ae9788c 100644 --- a/build.gradle +++ b/build.gradle @@ -61,7 +61,7 @@ ext { lz4Version = '1.8.0' micrometerDocsVersion = '1.0.3' micrometerVersion = '1.14.0-M1' - micrometerTracingVersion = '1.4.0-M1' + micrometerTracingVersion = '1.4.0-SNAPSHOT' mockitoVersion = '5.12.0' rabbitmqStreamVersion = '0.15.0' rabbitmqVersion = '5.21.0' From a76ffcd18d218c814b5cab96ffcbbc0914d60c5f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jul 2024 22:17:31 -0400 Subject: [PATCH 498/737] Bump kotlinVersion from 1.9.24 to 1.9.25 (#2764) Bumps `kotlinVersion` from 1.9.24 to 1.9.25. Updates `org.jetbrains.kotlin:kotlin-gradle-plugin` from 1.9.24 to 1.9.25 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/v1.9.25/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.9.24...v1.9.25) Updates `org.jetbrains.kotlin:kotlin-allopen` from 1.9.24 to 1.9.25 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/v1.9.25/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.9.24...v1.9.25) --- updated-dependencies: - dependency-name: org.jetbrains.kotlin:kotlin-gradle-plugin dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.jetbrains.kotlin:kotlin-allopen dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ca9ae9788c..28d86c574c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlinVersion = '1.9.24' + ext.kotlinVersion = '1.9.25' ext.isCI = System.getenv('GITHUB_ACTION') repositories { mavenCentral() From c7ac2c46f56056450db6035f62824ed4e7528af1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jul 2024 22:18:26 -0400 Subject: [PATCH 499/737] Bump io.projectreactor:reactor-bom from 2024.0.0-M4 to 2024.0.0-SNAPSHOT (#2766) Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2024.0.0-M4 to 2024.0.0-SNAPSHOT. - [Release notes](https://github.com/reactor/reactor/releases) - [Commits](https://github.com/reactor/reactor/commits) --- updated-dependencies: - dependency-name: io.projectreactor:reactor-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 28d86c574c..81e408b2d0 100644 --- a/build.gradle +++ b/build.gradle @@ -65,7 +65,7 @@ ext { mockitoVersion = '5.12.0' rabbitmqStreamVersion = '0.15.0' rabbitmqVersion = '5.21.0' - reactorVersion = '2024.0.0-M4' + reactorVersion = '2024.0.0-SNAPSHOT' snappyVersion = '1.1.10.5' springDataVersion = '2024.0.2' springRetryVersion = '2.0.6' From 7fb4ff2aa685da4a55944eb8ca00927bd2c2ffd2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jul 2024 22:19:54 -0400 Subject: [PATCH 500/737] Bump io.micrometer:micrometer-bom from 1.14.0-M1 to 1.14.0-SNAPSHOT (#2763) Bumps [io.micrometer:micrometer-bom](https://github.com/micrometer-metrics/micrometer) from 1.14.0-M1 to 1.14.0-SNAPSHOT. - [Release notes](https://github.com/micrometer-metrics/micrometer/releases) - [Commits](https://github.com/micrometer-metrics/micrometer/commits) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 81e408b2d0..6851d93419 100644 --- a/build.gradle +++ b/build.gradle @@ -60,7 +60,7 @@ ext { logbackVersion = '1.5.6' lz4Version = '1.8.0' micrometerDocsVersion = '1.0.3' - micrometerVersion = '1.14.0-M1' + micrometerVersion = '1.14.0-SNAPSHOT' micrometerTracingVersion = '1.4.0-SNAPSHOT' mockitoVersion = '5.12.0' rabbitmqStreamVersion = '0.15.0' From 78e9d0f0f4cecb6022e91460fc8b84e41fc4e2b8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jul 2024 22:20:22 -0400 Subject: [PATCH 501/737] Bump org.springframework.retry:spring-retry from 2.0.6 to 2.0.7 (#2765) Bumps [org.springframework.retry:spring-retry](https://github.com/spring-projects/spring-retry) from 2.0.6 to 2.0.7. - [Release notes](https://github.com/spring-projects/spring-retry/releases) - [Commits](https://github.com/spring-projects/spring-retry/compare/v2.0.6...v2.0.7) --- updated-dependencies: - dependency-name: org.springframework.retry:spring-retry dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 6851d93419..b3e00a2bda 100644 --- a/build.gradle +++ b/build.gradle @@ -68,7 +68,7 @@ ext { reactorVersion = '2024.0.0-SNAPSHOT' snappyVersion = '1.1.10.5' springDataVersion = '2024.0.2' - springRetryVersion = '2.0.6' + springRetryVersion = '2.0.7' springVersion = '6.2.0-SNAPSHOT' testcontainersVersion = '1.19.8' zstdJniVersion = '1.5.6-3' From 2448e0fc765fac176854d59acd3ea6d5b1433521 Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Mon, 22 Jul 2024 21:03:57 +0700 Subject: [PATCH 502/737] Fix README for Antora Also fix duplication typo in the `antora-playbook.yml` for `antora` section --- README.md | 6 +++--- src/reference/antora/antora-playbook.yml | 7 +++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 090a834c1c..cbd858118c 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,9 @@ To build api Javadoc (results will be in `build/api`): ./gradlew api -To build reference documentation (results will be in `build/reference`): +To build reference documentation (results will be in `build/site`): - ./gradlew reference + ./gradlew antora To build complete distribution including `-dist`, `-docs`, and `-schema` zip files (results will be in `build/distributions`) @@ -92,7 +92,7 @@ Here are some ways for you to get involved in the community: * Get involved with the Spring community on Stack Overflow by responding to questions and joining the debate. * Create Github issues for bugs and new features and comment and vote on the ones that you are interested in. -* Github is for social coding: if you want to write code, we encourage contributions through pull requests from [forks of this repository](https://help.github.com/forking/). +* Github is for social coding: if you want to write code, we encourage contributions through pull requests from [forks of this repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo). If you want to contribute code this way, please reference the specific Github issue you are addressing. Before we accept a non-trivial patch or pull request we will need you to sign the [contributor's agreement](https://cla.pivotal.io/sign/spring). diff --git a/src/reference/antora/antora-playbook.yml b/src/reference/antora/antora-playbook.yml index 3b67a1e807..333dfc2fb5 100644 --- a/src/reference/antora/antora-playbook.yml +++ b/src/reference/antora/antora-playbook.yml @@ -1,8 +1,7 @@ antora: - antora: - extensions: - - require: '@springio/antora-extensions' - root_component_name: 'amqp' + extensions: + - require: '@springio/antora-extensions' + root_component_name: 'amqp' site: title: Spring AMQP From 443ad4468e021a86a13728818a8101434b7788a6 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 24 Jul 2024 15:07:37 -0400 Subject: [PATCH 503/737] Use latest `spring-merge-dependabot-pr.yml` The `autoMergeSnapshots` is a new convenient input **Auto-cherry-pick to `3.1.x`** --- .github/workflows/merge-dependabot-pr.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/merge-dependabot-pr.yml b/.github/workflows/merge-dependabot-pr.yml index 42d8329d59..0b1d927d8e 100644 --- a/.github/workflows/merge-dependabot-pr.yml +++ b/.github/workflows/merge-dependabot-pr.yml @@ -12,6 +12,7 @@ jobs: merge-dependabot-pr: permissions: write-all - uses: spring-io/spring-github-workflows/.github/workflows/spring-merge-dependabot-pr.yml@v3 + uses: spring-io/spring-github-workflows/.github/workflows/spring-merge-dependabot-pr.yml@main with: - mergeArguments: --auto --squash \ No newline at end of file + mergeArguments: --auto --squash + autoMergeSnapshots: true \ No newline at end of file From d1af72a70154586235cfd303d84609dd43ecad89 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jul 2024 19:11:25 +0000 Subject: [PATCH 504/737] Bump com.github.luben:zstd-jni from 1.5.6-3 to 1.5.6-4 (#2774) Bumps [com.github.luben:zstd-jni](https://github.com/luben/zstd-jni) from 1.5.6-3 to 1.5.6-4. - [Commits](https://github.com/luben/zstd-jni/compare/v1.5.6-3...v1.5.6-4) --- updated-dependencies: - dependency-name: com.github.luben:zstd-jni dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b3e00a2bda..3aed114bf6 100644 --- a/build.gradle +++ b/build.gradle @@ -71,7 +71,7 @@ ext { springRetryVersion = '2.0.7' springVersion = '6.2.0-SNAPSHOT' testcontainersVersion = '1.19.8' - zstdJniVersion = '1.5.6-3' + zstdJniVersion = '1.5.6-4' javaProjects = subprojects - project(':spring-amqp-bom') } From 5020b1872db89940565836c9fbb6a39978ca6b43 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jul 2024 19:12:19 +0000 Subject: [PATCH 505/737] Bump com.gradle.develocity in the development-dependencies group (#2772) Bumps the development-dependencies group with 1 update: com.gradle.develocity. Updates `com.gradle.develocity` from 3.17.5 to 3.17.6 --- updated-dependencies: - dependency-name: com.gradle.develocity dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index 10e1e6d46e..20c7b45f6e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,7 +6,7 @@ pluginManagement { } plugins { - id 'com.gradle.develocity' version '3.17.5' + id 'com.gradle.develocity' version '3.17.6' id 'io.spring.develocity.conventions' version '0.0.19' } From e029edfcf351270d2623b75dbdc7478fb5a752f1 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 30 Jul 2024 11:49:13 -0400 Subject: [PATCH 506/737] Remove long deprecated `return-callback` XSD attribute It was also not handled properly by the `TemplateParser`. Supposed to map it into `returnsCallback` property of the `RabbitTemplate` --- .../amqp/rabbit/config/TemplateParser.java | 5 +---- .../amqp/rabbit/config/spring-rabbit.xsd | 15 --------------- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/TemplateParser.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/TemplateParser.java index dc8a6f9b2c..5259fa0e25 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/TemplateParser.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/TemplateParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -65,8 +65,6 @@ class TemplateParser extends AbstractSingleBeanDefinitionParser { private static final String MANDATORY_ATTRIBUTE = "mandatory"; - private static final String RETURN_CALLBACK_ATTRIBUTE = "return-callback"; - private static final String RETURNS_CALLBACK_ATTRIBUTE = "returns-callback"; private static final String CONFIRM_CALLBACK_ATTRIBUTE = "confirm-callback"; @@ -123,7 +121,6 @@ protected void doParse(Element element, ParserContext parserContext, BeanDefinit } NamespaceUtils.setValueIfAttributeDefined(builder, element, USE_TEMPORARY_REPLY_QUEUES_ATTRIBUTE); NamespaceUtils.setValueIfAttributeDefined(builder, element, REPLY_ADDRESS_ATTRIBUTE); - NamespaceUtils.setReferenceIfAttributeDefined(builder, element, RETURN_CALLBACK_ATTRIBUTE); NamespaceUtils.setReferenceIfAttributeDefined(builder, element, RETURNS_CALLBACK_ATTRIBUTE); NamespaceUtils.setReferenceIfAttributeDefined(builder, element, CONFIRM_CALLBACK_ATTRIBUTE); NamespaceUtils.setValueIfAttributeDefined(builder, element, CORRELATION_KEY); diff --git a/spring-rabbit/src/main/resources/org/springframework/amqp/rabbit/config/spring-rabbit.xsd b/spring-rabbit/src/main/resources/org/springframework/amqp/rabbit/config/spring-rabbit.xsd index 7f351ed349..3415435c15 100644 --- a/spring-rabbit/src/main/resources/org/springframework/amqp/rabbit/config/spring-rabbit.xsd +++ b/spring-rabbit/src/main/resources/org/springframework/amqp/rabbit/config/spring-rabbit.xsd @@ -1303,21 +1303,6 @@ ]]> - - - - - - - - - - Date: Tue, 30 Jul 2024 12:34:55 -0400 Subject: [PATCH 507/737] GH-2760: Fix endless loop in the `DirectMessageListenerContainer` Fixes: #2760 When the application starts, a message is sent to another service. During the start, the Rabbitmq broker crashed and the application began to write endlessly to the log line until it ran out of disk space. The line was written to the log is `DEBUG(org.springframework.amqp.rabbit.listener.DirectReplyToMessageListenerContainer): Consume from queue amq.rabbitmq.reply-to ignore, container stopping` **Auto-cherry-pick to `3.1.x`** --- .../rabbit/listener/DirectMessageListenerContainer.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java index b90c8c0f14..44ffc36f77 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java @@ -271,8 +271,8 @@ public void addQueues(Queue... queues) { Assert.noNullElements(queues, "'queues' cannot contain null elements"); try { Arrays.stream(queues) - .map(Queue::getActualName) - .forEach(this.removedQueues::remove); + .map(Queue::getActualName) + .forEach(this.removedQueues::remove); addQueues(Arrays.stream(queues).map(Queue::getName)); } catch (AmqpIOException e) { @@ -340,8 +340,9 @@ private void adjustConsumers(int newCount) { checkStartState(); this.consumersToRestart.clear(); for (String queue : getQueueNames()) { - while (this.consumersByQueue.get(queue) == null - || this.consumersByQueue.get(queue).size() < newCount) { // NOSONAR never null + while (isActive() && + (this.consumersByQueue.get(queue) == null + || this.consumersByQueue.get(queue).size() < newCount)) { // NOSONAR never null List cBQ = this.consumersByQueue.get(queue); int index = 0; if (cBQ != null) { From 02c3c82e3dde5a6e61c61cb8ca7d4d8c56257748 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 Aug 2024 23:20:24 -0400 Subject: [PATCH 508/737] Bump org.junit:junit-bom from 5.11.0-M2 to 5.11.0-RC1 (#2776) Bumps [org.junit:junit-bom](https://github.com/junit-team/junit5) from 5.11.0-M2 to 5.11.0-RC1. - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.11.0-M2...r5.11.0-RC1) --- updated-dependencies: - dependency-name: org.junit:junit-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 3aed114bf6..7d45cbe3f4 100644 --- a/build.gradle +++ b/build.gradle @@ -54,7 +54,7 @@ ext { jacksonBomVersion = '2.17.2' jaywayJsonPathVersion = '2.9.0' junit4Version = '4.13.2' - junitJupiterVersion = '5.11.0-M2' + junitJupiterVersion = '5.11.0-RC1' kotlinCoroutinesVersion = '1.8.1' log4jVersion = '2.23.1' logbackVersion = '1.5.6' From 43bf668503af9fc48a404206f000734ec86bd4eb Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 5 Aug 2024 12:49:22 -0400 Subject: [PATCH 509/737] Resolve Gradle deprecation for `Feature` * Use `configuration` and `addVariantsFromConfiguration()` instead to expose `optional` dependencies * Fix `api` to resolve `classpath` on demand * Upgrade to Gradle `8.9` --- build.gradle | 157 ++++++++++------------- gradle/wrapper/gradle-wrapper.jar | Bin 43453 -> 43504 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 7 +- gradlew.bat | 2 + 5 files changed, 78 insertions(+), 92 deletions(-) diff --git a/build.gradle b/build.gradle index 7d45cbe3f4..7a55d61f82 100644 --- a/build.gradle +++ b/build.gradle @@ -42,6 +42,7 @@ ext { files(grgit.status().unstaged.modified) .filter { f -> f.name.endsWith('.java') || f.name.endsWith('.kt') } } + modifiedFiles.finalizeValueOnRead() assertjVersion = '3.26.3' assertkVersion = '0.28.1' @@ -76,8 +77,6 @@ ext { javaProjects = subprojects - project(':spring-amqp-bom') } - - antora { version = '3.2.0-alpha.2' playbook = file('src/reference/antora/antora-playbook.yml') @@ -98,12 +97,12 @@ tasks.named('generateAntoraYml') { baseAntoraYmlFile = file('src/reference/antora/antora.yml') } -tasks.create(name: 'createAntoraPartials', type: Sync) { +tasks.register('createAntoraPartials', Sync) { from { tasks.filterMetricsDocsContent.outputs } into layout.buildDirectory.dir('generated-antora-resources/modules/ROOT/partials') } -tasks.create('generateAntoraResources') { +tasks.register('generateAntoraResources') { dependsOn 'createAntoraPartials' dependsOn 'generateAntoraYml' } @@ -171,15 +170,24 @@ configure(javaProjects) { subproject -> apply plugin: 'kotlin' apply plugin: 'kotlin-spring' + configurations { + optional + } + + components.java.with { + it.addVariantsFromConfiguration(configurations.optional) { + mapToOptional() + } + } + java { withJavadocJar() withSourcesJar() - registerFeature('optional') { - usingSourceSet(sourceSets.main) - } - registerFeature('provided') { - usingSourceSet(sourceSets.main) - } + } + + sourceSets.all { + compileClasspath += configurations.optional + runtimeClasspath += configurations.optional } compileJava { @@ -228,8 +236,6 @@ configure(javaProjects) { subproject -> testImplementation 'org.jetbrains.kotlin:kotlin-reflect' testImplementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' - - } // enable all compiler warnings; individual projects may customize further @@ -241,23 +247,11 @@ configure(javaProjects) { subproject -> mavenJava(MavenPublication) { suppressAllPomMetadataWarnings() from components.java - pom.withXml { - def pomDeps = asNode().dependencies.first() - subproject.configurations.providedImplementation.allDependencies.each { dep -> - pomDeps.remove(pomDeps.'*'.find { it.artifactId.text() == dep.name }) - pomDeps.appendNode('dependency').with { - it.appendNode('groupId', dep.group) - it.appendNode('artifactId', dep.name) - it.appendNode('version', dep.version) - it.appendNode('scope', 'provided') - } - } - } } } } - task updateCopyrights { + tasks.register('updateCopyrights') { onlyIf { !isCI } inputs.files(modifiedFiles.filter { f -> f.path.contains(subproject.name) }) outputs.dir('build/classes') @@ -310,7 +304,9 @@ configure(javaProjects) { subproject -> } } - task testAll(type: Test, dependsOn: check) + tasks.register('testAll', Test) { + dependsOn check + } gradle.taskGraph.whenReady { graph -> if (graph.hasTask(testAll)) { @@ -318,7 +314,7 @@ configure(javaProjects) { subproject -> } } - tasks.withType(Test).all { + tasks.withType(Test).configureEach { // suppress all console output during testing unless running `gradle -i` logging.captureStandardOutput(LogLevel.INFO) @@ -360,39 +356,37 @@ configure(javaProjects) { subproject -> } check.dependsOn javadoc - } project('spring-amqp') { description = 'Spring AMQP Core' dependencies { - api 'org.springframework:spring-core' - optionalApi 'org.springframework:spring-messaging' - optionalApi 'org.springframework:spring-oxm' - optionalApi 'org.springframework:spring-context' + optional 'org.springframework:spring-messaging' + optional 'org.springframework:spring-oxm' + optional 'org.springframework:spring-context' api ("org.springframework.retry:spring-retry:$springRetryVersion") { exclude group: 'org.springframework' } - optionalApi 'com.fasterxml.jackson.core:jackson-core' - optionalApi 'com.fasterxml.jackson.core:jackson-databind' - optionalApi 'com.fasterxml.jackson.core:jackson-annotations' - optionalApi 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' - optionalApi 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8' - optionalApi 'com.fasterxml.jackson.module:jackson-module-parameter-names' - optionalApi 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' - optionalApi 'com.fasterxml.jackson.datatype:jackson-datatype-joda' - optionalApi ('com.fasterxml.jackson.module:jackson-module-kotlin') { + optional 'com.fasterxml.jackson.core:jackson-core' + optional 'com.fasterxml.jackson.core:jackson-databind' + optional 'com.fasterxml.jackson.core:jackson-annotations' + optional 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' + optional 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8' + optional 'com.fasterxml.jackson.module:jackson-module-parameter-names' + optional 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' + optional 'com.fasterxml.jackson.datatype:jackson-datatype-joda' + optional ('com.fasterxml.jackson.module:jackson-module-kotlin') { exclude group: 'org.jetbrains.kotlin' } // Spring Data projection message binding support - optionalApi ('org.springframework.data:spring-data-commons') { + optional ('org.springframework.data:spring-data-commons') { exclude group: 'org.springframework' exclude group: 'io.micrometer' } - optionalApi "com.jayway.jsonpath:json-path:$jaywayJsonPathVersion" + optional "com.jayway.jsonpath:json-path:$jaywayJsonPathVersion" testImplementation "org.assertj:assertj-core:$assertjVersion" } @@ -425,34 +419,29 @@ project('spring-amqp-bom') { project('spring-rabbit') { description = 'Spring RabbitMQ Support' - configurations { - adoc - } - dependencies { - api project(':spring-amqp') api "com.rabbitmq:amqp-client:$rabbitmqVersion" - optionalApi 'org.springframework:spring-aop' api 'org.springframework:spring-context' api 'org.springframework:spring-messaging' api 'org.springframework:spring-tx' - optionalApi 'org.springframework:spring-webflux' - optionalApi "org.apache.httpcomponents.client5:httpclient5:$commonsHttpClientVersion" - optionalApi 'io.projectreactor:reactor-core' - optionalApi 'io.projectreactor.netty:reactor-netty-http' - optionalApi "ch.qos.logback:logback-classic:$logbackVersion" - optionalApi 'org.apache.logging.log4j:log4j-core' - optionalApi 'io.micrometer:micrometer-core' api 'io.micrometer:micrometer-observation' - optionalApi 'io.micrometer:micrometer-tracing' + optional 'org.springframework:spring-aop' + optional 'org.springframework:spring-webflux' + optional "org.apache.httpcomponents.client5:httpclient5:$commonsHttpClientVersion" + optional 'io.projectreactor:reactor-core' + optional 'io.projectreactor.netty:reactor-netty-http' + optional "ch.qos.logback:logback-classic:$logbackVersion" + optional 'org.apache.logging.log4j:log4j-core' + optional 'io.micrometer:micrometer-core' + optional 'io.micrometer:micrometer-tracing' // Spring Data projection message binding support - optionalApi ("org.springframework.data:spring-data-commons") { + optional ("org.springframework.data:spring-data-commons") { exclude group: 'org.springframework' } - optionalApi "com.jayway.jsonpath:json-path:$jaywayJsonPathVersion" - optionalApi "org.apache.commons:commons-pool2:$commonsPoolVersion" - optionalApi "org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlinCoroutinesVersion" + optional "com.jayway.jsonpath:json-path:$jaywayJsonPathVersion" + optional "org.apache.commons:commons-pool2:$commonsPoolVersion" + optional "org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlinCoroutinesVersion" testApi project(':spring-rabbit-junit') testImplementation("com.willowtreeapps.assertk:assertk-jvm:$assertkVersion") @@ -470,9 +459,7 @@ project('spring-rabbit') { testRuntimeOnly 'com.fasterxml.jackson.module:jackson-module-kotlin' testRuntimeOnly ("junit:junit:$junit4Version") { exclude group: 'org.hamcrest', module: 'hamcrest-core' - } - } compileKotlin { @@ -480,17 +467,15 @@ project('spring-rabbit') { allWarningsAsErrors = true } } - } project('spring-rabbit-stream') { description = 'Spring RabbitMQ Stream Support' dependencies { - api project(':spring-rabbit') api "com.rabbitmq:stream-client:$rabbitmqStreamVersion" - optionalApi 'io.micrometer:micrometer-core' + optional 'io.micrometer:micrometer-core' testApi project(':spring-rabbit-junit') testRuntimeOnly 'com.fasterxml.jackson.core:jackson-core' @@ -510,37 +495,33 @@ project('spring-rabbit-stream') { testImplementation 'io.micrometer:micrometer-tracing-test' testImplementation 'io.micrometer:micrometer-tracing-integration-test' } - } project('spring-rabbit-junit') { description = 'Spring Rabbit JUnit Support' dependencies { // no spring-amqp dependencies allowed - api 'org.springframework:spring-core' api 'org.springframework:spring-test' - optionalApi ("junit:junit:$junit4Version") { + optional ("junit:junit:$junit4Version") { exclude group: 'org.hamcrest', module: 'hamcrest-core' } api "com.rabbitmq:amqp-client:$rabbitmqVersion" api 'org.springframework:spring-web' api 'org.junit.jupiter:junit-jupiter-api' api "org.assertj:assertj-core:$assertjVersion" - optionalApi "org.testcontainers:rabbitmq" - optionalApi "org.testcontainers:junit-jupiter" - optionalApi "ch.qos.logback:logback-classic:$logbackVersion" - optionalApi 'org.apache.logging.log4j:log4j-core' + optional "org.testcontainers:rabbitmq" + optional "org.testcontainers:junit-jupiter" + optional "ch.qos.logback:logback-classic:$logbackVersion" + optional 'org.apache.logging.log4j:log4j-core' compileOnly 'org.apiguardian:apiguardian-api:1.0.0' } - } project('spring-rabbit-test') { description = 'Spring Rabbit Test Support' dependencies { - api project(':spring-rabbit') api project(':spring-rabbit-junit') api "org.hamcrest:hamcrest-library:$hamcrestVersion" @@ -548,7 +529,6 @@ project('spring-rabbit-test') { api "org.mockito:mockito-core:$mockitoVersion" testImplementation project(':spring-rabbit').sourceSets.test.output } - } configurations { @@ -562,7 +542,7 @@ dependencies { def observationInputDir = file('build/docs/microsources').absolutePath def generatedDocsDir = file('build/docs/generated').absolutePath -task copyObservation(type: Copy) { +tasks.register('copyObservation', Copy) { from file('spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer').absolutePath from file('spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer').absolutePath include '*.java' @@ -570,7 +550,7 @@ task copyObservation(type: Copy) { into observationInputDir } -task generateObservabilityDocs(type: JavaExec) { +tasks.register('generateObservabilityDocs', JavaExec) { dependsOn copyObservation mainClass = 'io.micrometer.docs.DocsGeneratorCommand' inputs.dir(observationInputDir) @@ -579,7 +559,7 @@ task generateObservabilityDocs(type: JavaExec) { args observationInputDir, /.+/, generatedDocsDir } -task filterMetricsDocsContent(type: Copy) { +tasks.register('filterMetricsDocsContent', Copy) { dependsOn generateObservabilityDocs from generatedDocsDir include '_*.adoc' @@ -588,7 +568,7 @@ task filterMetricsDocsContent(type: Copy) { filter { line -> line.replaceAll('org.springframework.*.micrometer.', '').replaceAll('^Fully qualified n', 'N') } } -task api(type: Javadoc) { +tasks.register('api', Javadoc) { group = 'Documentation' description = 'Generates aggregated Javadoc API documentation.' title = "${rootProject.description} ${version} API" @@ -607,13 +587,11 @@ task api(type: Javadoc) { source javaProjects.collect { project -> project.sourceSets.main.allJava } - destinationDir = new File(buildDir, 'api') - classpath = files(javaProjects.collect { project -> - project.sourceSets.main.compileClasspath - }) + destinationDir = new File('build', 'api') + classpath = files().from { files(javaProjects.collect { it.sourceSets.main.compileClasspath }) } } -task schemaZip(type: Zip) { +tasks.register('schemaZip', Zip) { group = 'Distribution' archiveClassifier = 'schema' description = "Builds -${archiveClassifier} archive containing all " + @@ -651,7 +629,7 @@ task schemaZip(type: Zip) { } } -task docsZip(type: Zip) { +tasks.register('docsZip', Zip) { group = 'Distribution' archiveClassifier = 'docs' description = "Builds -${archiveClassifier} archive containing api and reference " + @@ -666,7 +644,9 @@ task docsZip(type: Zip) { } } -task distZip(type: Zip, dependsOn: [docsZip, schemaZip]) { +tasks.register('distZip', Zip) { + dependsOn 'docsZip' + dependsOn 'schemaZip' group = 'Distribution' archiveClassifier = 'dist' description = "Builds -${archiveClassifier} archive, containing all jars and docs, " + @@ -746,7 +726,8 @@ artifacts { archives schemaZip } -task dist(dependsOn: assemble) { +tasks.register('dist') { + dependsOn assemble group = 'Distribution' description = 'Builds -dist, -docs and -schema distribution archives.' } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e6441136f3d4ba8a0da8d277868979cfbc8ad796..2c3521197d7c4586c843d1d3e9090525f1898cde 100644 GIT binary patch delta 8703 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 Ojj7^59?Sdh$^Qpp*~F>< delta 8662 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 381baa9cef..68e8816d71 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a4269..f5feea6d6b 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 7101f8e467..9b42019c79 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## From c1d527adcbdbdb2290af4295af8e7bd67f6b2976 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 5 Aug 2024 14:20:12 -0400 Subject: [PATCH 510/737] Fix Gradle configuration for `api` task --- build.gradle | 91 ++++++++++++++++++++++++++-------------------------- 1 file changed, 45 insertions(+), 46 deletions(-) diff --git a/build.gradle b/build.gradle index 7a55d61f82..9890a94062 100644 --- a/build.gradle +++ b/build.gradle @@ -144,7 +144,6 @@ allprojects { maven { url 'https://repo.spring.io/milestone' } if (version.endsWith('-SNAPSHOT')) { maven { url 'https://repo.spring.io/snapshot' } - // maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } } // maven { url 'https://repo.spring.io/libs-staging-local' } } @@ -161,23 +160,30 @@ ext { configure(javaProjects) { subproject -> apply plugin: 'java-library' - apply plugin: 'java' - apply from: "${rootProject.projectDir}/gradle/publish-maven.gradle" apply plugin: 'eclipse' apply plugin: 'idea' - apply plugin: 'project-report' apply plugin: 'checkstyle' apply plugin: 'kotlin' apply plugin: 'kotlin-spring' + apply from: "${rootProject.projectDir}/gradle/publish-maven.gradle" + + def scopeAttribute = Attribute.of('dependency.scope', String) + configurations { - optional + optional { + attributes { + attribute(scopeAttribute, 'optional') + } + } + + compileClasspath.extendsFrom(optional) + testCompileClasspath.extendsFrom(optional) + testRuntimeClasspath.extendsFrom(optional) } - components.java.with { - it.addVariantsFromConfiguration(configurations.optional) { + components.java.addVariantsFromConfiguration(configurations.optional) { mapToOptional() - } } java { @@ -185,19 +191,13 @@ configure(javaProjects) { subproject -> withSourcesJar() } - sourceSets.all { - compileClasspath += configurations.optional - runtimeClasspath += configurations.optional - } - compileJava { - sourceCompatibility = 17 - targetCompatibility = 17 + options.release = 17 } compileTestJava { - sourceCompatibility = 17 - targetCompatibility = 17 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 options.encoding = 'UTF-8' } @@ -209,6 +209,10 @@ configure(javaProjects) { subproject -> // dependencies that are common across all java projects dependencies { + attributesSchema { + attribute(scopeAttribute) + } + def spotbugsAnnotations = "com.github.spotbugs:spotbugs-annotations:${spotbugs.toolVersion.get()}" compileOnly spotbugsAnnotations testCompileOnly spotbugsAnnotations @@ -356,6 +360,7 @@ configure(javaProjects) { subproject -> } check.dependsOn javadoc + } project('spring-amqp') { @@ -363,13 +368,12 @@ project('spring-amqp') { dependencies { api 'org.springframework:spring-core' - optional 'org.springframework:spring-messaging' - optional 'org.springframework:spring-oxm' - optional 'org.springframework:spring-context' api ("org.springframework.retry:spring-retry:$springRetryVersion") { exclude group: 'org.springframework' } - + optional 'org.springframework:spring-messaging' + optional 'org.springframework:spring-oxm' + optional 'org.springframework:spring-context' optional 'com.fasterxml.jackson.core:jackson-core' optional 'com.fasterxml.jackson.core:jackson-databind' optional 'com.fasterxml.jackson.core:jackson-annotations' @@ -503,13 +507,13 @@ project('spring-rabbit-junit') { dependencies { // no spring-amqp dependencies allowed api 'org.springframework:spring-core' api 'org.springframework:spring-test' - optional ("junit:junit:$junit4Version") { - exclude group: 'org.hamcrest', module: 'hamcrest-core' - } api "com.rabbitmq:amqp-client:$rabbitmqVersion" api 'org.springframework:spring-web' api 'org.junit.jupiter:junit-jupiter-api' api "org.assertj:assertj-core:$assertjVersion" + optional ("junit:junit:$junit4Version") { + exclude group: 'org.hamcrest', module: 'hamcrest-core' + } optional "org.testcontainers:rabbitmq" optional "org.testcontainers:junit-jupiter" optional "ch.qos.logback:logback-classic:$logbackVersion" @@ -616,7 +620,7 @@ tasks.register('schemaZip', Zip) { } assert xsdFile != null if (!files.contains(xsdFile.path)) { - into ("${shortName}") { + into("${shortName}") { from xsdFile.path rename { String fileName -> String[] versionNumbers = project.version.split(/\./, 3) @@ -630,27 +634,27 @@ tasks.register('schemaZip', Zip) { } tasks.register('docsZip', Zip) { - group = 'Distribution' - archiveClassifier = 'docs' - description = "Builds -${archiveClassifier} archive containing api and reference " + + group = 'Distribution' + archiveClassifier = 'docs' + description = "Builds -${archiveClassifier} archive containing api and reference " + "for deployment at static.springframework.org/spring-integration/docs." - from('src/dist') { - include 'changelog.txt' - } + from('src/dist') { + include 'changelog.txt' + } - from (api) { - into 'api' - } + from(api) { + into 'api' + } } tasks.register('distZip', Zip) { - dependsOn 'docsZip' - dependsOn 'schemaZip' + dependsOn docsZip + dependsOn schemaZip group = 'Distribution' archiveClassifier = 'dist' description = "Builds -${archiveClassifier} archive, containing all jars and docs, " + - "suitable for community download page." + "suitable for community download page." ext.baseDir = "${project.name}-${project.version}"; @@ -674,7 +678,7 @@ tasks.register('distZip', Zip) { } javaProjects.each { subproject -> - into ("${baseDir}/libs") { + into("${baseDir}/libs") { from subproject.jar from subproject.sourcesJar from subproject.javadocJar @@ -690,11 +694,12 @@ tasks.register('distZip', Zip) { // Create an optional "with dependencies" distribution. // Not published by default; only for use when building from source. -task depsZip(type: Zip, dependsOn: distZip) { zipTask -> +tasks.register('depsZip', Zip) { + dependsOn distZip group = 'Distribution' archiveClassifier = 'dist-with-deps' description = "Builds -${archiveClassifier} archive, containing everything " + - "in the -${distZip.archiveClassifier} archive plus all dependencies." + "in the -${distZip.archiveClassifier} archive plus all dependencies." from zipTree(distZip.archiveFile) @@ -720,12 +725,6 @@ task depsZip(type: Zip, dependsOn: distZip) { zipTask -> tasks.build.dependsOn assemble -artifacts { - archives distZip - archives docsZip - archives schemaZip -} - tasks.register('dist') { dependsOn assemble group = 'Distribution' From db812c8198f714812040d0133ce2a61710476753 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 5 Aug 2024 16:30:46 -0400 Subject: [PATCH 511/737] Use `io.freefair.aggregate-javadoc` plugin --- build.gradle | 82 +++++++++++++++++++++++++++++----------------------- 1 file changed, 46 insertions(+), 36 deletions(-) diff --git a/build.gradle b/build.gradle index 9890a94062..a46497bf19 100644 --- a/build.gradle +++ b/build.gradle @@ -24,24 +24,25 @@ plugins { id 'org.antora' version '1.0.0' id 'io.spring.antora.generate-antora-yml' version '0.0.1' id 'com.github.spotbugs' version '6.0.19' + id 'io.freefair.aggregate-javadoc' version '8.6' } description = 'Spring AMQP' ext { linkHomepage = 'https://projects.spring.io/spring-amqp' - linkCi = 'https://build.spring.io/browse/AMQP' - linkIssue = 'https://jira.spring.io/browse/AMQP' - linkScmUrl = 'https://github.com/spring-projects/spring-amqp' - linkScmConnection = 'git://github.com/spring-projects/spring-amqp.git' + linkCi = 'https://build.spring.io/browse/AMQP' + linkIssue = 'https://jira.spring.io/browse/AMQP' + linkScmUrl = 'https://github.com/spring-projects/spring-amqp' + linkScmConnection = 'git://github.com/spring-projects/spring-amqp.git' linkScmDevConnection = 'git@github.com:spring-projects/spring-amqp.git' modifiedFiles = - files() - .from { - files(grgit.status().unstaged.modified) - .filter { f -> f.name.endsWith('.java') || f.name.endsWith('.kt') } - } + files() + .from { + files(grgit.status().unstaged.modified) + .filter { f -> f.name.endsWith('.java') || f.name.endsWith('.kt') } + } modifiedFiles.finalizeValueOnRead() assertjVersion = '3.26.3' @@ -91,9 +92,9 @@ antora { } tasks.named('generateAntoraYml') { - asciidocAttributes = project.provider( { - return ['project-version' : project.version ] - } ) + asciidocAttributes = project.provider({ + return ['project-version': project.version] + }) baseAntoraYmlFile = file('src/reference/antora/antora.yml') } @@ -152,9 +153,9 @@ allprojects { ext { expandPlaceholders = '**/quick-tour.xml' javadocLinks = [ - 'https://docs.oracle.com/en/java/javase/17/docs/api/', - 'https://jakarta.ee/specifications/platform/9/apidocs/', - 'https://docs.spring.io/spring-framework/docs/current/javadoc-api/' + 'https://docs.oracle.com/en/java/javase/17/docs/api/', + 'https://jakarta.ee/specifications/platform/9/apidocs/', + 'https://docs.spring.io/spring-framework/docs/current/javadoc-api/' ] as String[] } @@ -166,7 +167,7 @@ configure(javaProjects) { subproject -> apply plugin: 'kotlin' apply plugin: 'kotlin-spring' - apply from: "${rootProject.projectDir}/gradle/publish-maven.gradle" + apply from: "${rootProject.projectDir}/gradle/publish-maven.gradle" def scopeAttribute = Attribute.of('dependency.scope', String) @@ -183,7 +184,7 @@ configure(javaProjects) { subproject -> } components.java.addVariantsFromConfiguration(configurations.optional) { - mapToOptional() + mapToOptional() } java { @@ -219,7 +220,7 @@ configure(javaProjects) { subproject -> testImplementation 'org.apache.logging.log4j:log4j-core' testImplementation "org.hamcrest:hamcrest-core:$hamcrestVersion" - testImplementation ("org.mockito:mockito-core:$mockitoVersion") { + testImplementation("org.mockito:mockito-core:$mockitoVersion") { exclude group: 'org.hamcrest', module: 'hamcrest-core' } testImplementation "org.mockito:mockito-junit-jupiter:$mockitoVersion" @@ -240,6 +241,8 @@ configure(javaProjects) { subproject -> testImplementation 'org.jetbrains.kotlin:kotlin-reflect' testImplementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + + } // enable all compiler warnings; individual projects may customize further @@ -368,7 +371,7 @@ project('spring-amqp') { dependencies { api 'org.springframework:spring-core' - api ("org.springframework.retry:spring-retry:$springRetryVersion") { + api("org.springframework.retry:spring-retry:$springRetryVersion") { exclude group: 'org.springframework' } optional 'org.springframework:spring-messaging' @@ -382,11 +385,11 @@ project('spring-amqp') { optional 'com.fasterxml.jackson.module:jackson-module-parameter-names' optional 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' optional 'com.fasterxml.jackson.datatype:jackson-datatype-joda' - optional ('com.fasterxml.jackson.module:jackson-module-kotlin') { + optional('com.fasterxml.jackson.module:jackson-module-kotlin') { exclude group: 'org.jetbrains.kotlin' } // Spring Data projection message binding support - optional ('org.springframework.data:spring-data-commons') { + optional('org.springframework.data:spring-data-commons') { exclude group: 'org.springframework' exclude group: 'io.micrometer' } @@ -440,7 +443,7 @@ project('spring-rabbit') { optional 'io.micrometer:micrometer-core' optional 'io.micrometer:micrometer-tracing' // Spring Data projection message binding support - optional ("org.springframework.data:spring-data-commons") { + optional("org.springframework.data:spring-data-commons") { exclude group: 'org.springframework' } optional "com.jayway.jsonpath:json-path:$jaywayJsonPathVersion" @@ -461,7 +464,7 @@ project('spring-rabbit') { testRuntimeOnly 'com.fasterxml.jackson.core:jackson-databind' testRuntimeOnly 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' testRuntimeOnly 'com.fasterxml.jackson.module:jackson-module-kotlin' - testRuntimeOnly ("junit:junit:$junit4Version") { + testRuntimeOnly("junit:junit:$junit4Version") { exclude group: 'org.hamcrest', module: 'hamcrest-core' } } @@ -511,7 +514,7 @@ project('spring-rabbit-junit') { api 'org.springframework:spring-web' api 'org.junit.jupiter:junit-jupiter-api' api "org.assertj:assertj-core:$assertjVersion" - optional ("junit:junit:$junit4Version") { + optional("junit:junit:$junit4Version") { exclude group: 'org.hamcrest', module: 'hamcrest-core' } optional "org.testcontainers:rabbitmq" @@ -572,15 +575,19 @@ tasks.register('filterMetricsDocsContent', Copy) { filter { line -> line.replaceAll('org.springframework.*.micrometer.', '').replaceAll('^Fully qualified n', 'N') } } -tasks.register('api', Javadoc) { - group = 'Documentation' - description = 'Generates aggregated Javadoc API documentation.' +dependencies { + javaProjects.each { + javadoc it + } +} + +javadoc { title = "${rootProject.description} ${version} API" options { encoding = 'UTF-8' memberLevel = JavadocMemberLevel.PROTECTED author = true - header = rootProject.description + header = project.description use = true overview = 'src/api/overview.html' splitIndex = true @@ -588,22 +595,25 @@ tasks.register('api', Javadoc) { addBooleanOption('Xdoclint:syntax', true) // only check syntax with doclint } - source javaProjects.collect { project -> - project.sourceSets.main.allJava - } - destinationDir = new File('build', 'api') + destinationDir = file('build/api') classpath = files().from { files(javaProjects.collect { it.sourceSets.main.compileClasspath }) } } +tasks.register('api') { + group = 'Documentation' + description = 'Generates aggregated Javadoc API documentation.' + dependsOn javadoc +} + tasks.register('schemaZip', Zip) { group = 'Distribution' archiveClassifier = 'schema' description = "Builds -${archiveClassifier} archive containing all " + - "XSDs for deployment at static.springframework.org/schema." + "XSDs for deployment at static.springframework.org/schema." javaProjects.each { subproject -> - def Set files = new HashSet() - def Properties schemas = new Properties(); + Set files = new HashSet() + Properties schemas = new Properties(); def shortName = subproject.name.replaceFirst("${rootProject.name}-", '') if (subproject.name.endsWith('-rabbit')) { @@ -643,7 +653,7 @@ tasks.register('docsZip', Zip) { include 'changelog.txt' } - from(api) { + from(javadoc) { into 'api' } } From dc16c6ed98839aeb07ff327dc8dd66c48db63903 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 Aug 2024 22:44:04 -0400 Subject: [PATCH 512/737] Bump org.xerial.snappy:snappy-java from 1.1.10.5 to 1.1.10.6 (#2780) Bumps [org.xerial.snappy:snappy-java](https://github.com/xerial/snappy-java) from 1.1.10.5 to 1.1.10.6. - [Release notes](https://github.com/xerial/snappy-java/releases) - [Commits](https://github.com/xerial/snappy-java/compare/v1.1.10.5...v1.1.10.6) --- updated-dependencies: - dependency-name: org.xerial.snappy:snappy-java dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a46497bf19..6297c2f434 100644 --- a/build.gradle +++ b/build.gradle @@ -68,7 +68,7 @@ ext { rabbitmqStreamVersion = '0.15.0' rabbitmqVersion = '5.21.0' reactorVersion = '2024.0.0-SNAPSHOT' - snappyVersion = '1.1.10.5' + snappyVersion = '1.1.10.6' springDataVersion = '2024.0.2' springRetryVersion = '2.0.7' springVersion = '6.2.0-SNAPSHOT' From d8ccbd97cc949e54a94319d9035f2f81f3c574dd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 10 Aug 2024 02:46:23 +0000 Subject: [PATCH 513/737] Bump the development-dependencies group with 2 updates (#2779) Bumps the development-dependencies group with 2 updates: [org.awaitility:awaitility](https://github.com/awaitility/awaitility) and com.github.spotbugs. Updates `org.awaitility:awaitility` from 4.2.1 to 4.2.2 - [Changelog](https://github.com/awaitility/awaitility/blob/master/changelog.txt) - [Commits](https://github.com/awaitility/awaitility/compare/awaitility-4.2.1...awaitility-4.2.2) Updates `com.github.spotbugs` from 6.0.19 to 6.0.20 --- updated-dependencies: - dependency-name: org.awaitility:awaitility dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies - dependency-name: com.github.spotbugs dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 6297c2f434..4e5dcfd011 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ plugins { id 'io.spring.dependency-management' version '1.1.6' apply false id 'org.antora' version '1.0.0' id 'io.spring.antora.generate-antora-yml' version '0.0.1' - id 'com.github.spotbugs' version '6.0.19' + id 'com.github.spotbugs' version '6.0.20' id 'io.freefair.aggregate-javadoc' version '8.6' } @@ -47,7 +47,7 @@ ext { assertjVersion = '3.26.3' assertkVersion = '0.28.1' - awaitilityVersion = '4.2.1' + awaitilityVersion = '4.2.2' commonsCompressVersion = '1.26.2' commonsHttpClientVersion = '5.3.1' commonsPoolVersion = '2.12.0' From 2789038f4d69196df57110d664073aadd8b7dcc0 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 16 Aug 2024 12:15:10 -0400 Subject: [PATCH 514/737] Revert back to `registerFeature('optional')` **Auto-cherry-pick to `3.1.x`** With a `components.java.addVariantsFromConfiguration(configurations.optional)` and `io.spring.dependency-management` combination the version is not published for `optional` dependencies. Does not look like there is an easy way to extract that version somehow. So, revert back to the `registerFeature('optional')` for time being. Even if it is reported as deprecated in Gradle --- build.gradle | 92 ++++++++++++++++++++-------------------------------- 1 file changed, 36 insertions(+), 56 deletions(-) diff --git a/build.gradle b/build.gradle index 4e5dcfd011..7d62748cd2 100644 --- a/build.gradle +++ b/build.gradle @@ -169,27 +169,12 @@ configure(javaProjects) { subproject -> apply from: "${rootProject.projectDir}/gradle/publish-maven.gradle" - def scopeAttribute = Attribute.of('dependency.scope', String) - - configurations { - optional { - attributes { - attribute(scopeAttribute, 'optional') - } - } - - compileClasspath.extendsFrom(optional) - testCompileClasspath.extendsFrom(optional) - testRuntimeClasspath.extendsFrom(optional) - } - - components.java.addVariantsFromConfiguration(configurations.optional) { - mapToOptional() - } - java { withJavadocJar() withSourcesJar() + registerFeature('optional') { + usingSourceSet(sourceSets.main) + } } compileJava { @@ -210,10 +195,6 @@ configure(javaProjects) { subproject -> // dependencies that are common across all java projects dependencies { - attributesSchema { - attribute(scopeAttribute) - } - def spotbugsAnnotations = "com.github.spotbugs:spotbugs-annotations:${spotbugs.toolVersion.get()}" compileOnly spotbugsAnnotations testCompileOnly spotbugsAnnotations @@ -261,7 +242,6 @@ configure(javaProjects) { subproject -> tasks.register('updateCopyrights') { onlyIf { !isCI } inputs.files(modifiedFiles.filter { f -> f.path.contains(subproject.name) }) - outputs.dir('build/classes') doLast { def now = Calendar.instance.get(Calendar.YEAR) as String @@ -374,26 +354,26 @@ project('spring-amqp') { api("org.springframework.retry:spring-retry:$springRetryVersion") { exclude group: 'org.springframework' } - optional 'org.springframework:spring-messaging' - optional 'org.springframework:spring-oxm' - optional 'org.springframework:spring-context' - optional 'com.fasterxml.jackson.core:jackson-core' - optional 'com.fasterxml.jackson.core:jackson-databind' - optional 'com.fasterxml.jackson.core:jackson-annotations' - optional 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' - optional 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8' - optional 'com.fasterxml.jackson.module:jackson-module-parameter-names' - optional 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' - optional 'com.fasterxml.jackson.datatype:jackson-datatype-joda' - optional('com.fasterxml.jackson.module:jackson-module-kotlin') { + optionalApi 'org.springframework:spring-messaging' + optionalApi 'org.springframework:spring-oxm' + optionalApi 'org.springframework:spring-context' + optionalApi 'com.fasterxml.jackson.core:jackson-core' + optionalApi 'com.fasterxml.jackson.core:jackson-databind' + optionalApi 'com.fasterxml.jackson.core:jackson-annotations' + optionalApi 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' + optionalApi 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8' + optionalApi 'com.fasterxml.jackson.module:jackson-module-parameter-names' + optionalApi 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' + optionalApi 'com.fasterxml.jackson.datatype:jackson-datatype-joda' + optionalApi('com.fasterxml.jackson.module:jackson-module-kotlin') { exclude group: 'org.jetbrains.kotlin' } // Spring Data projection message binding support - optional('org.springframework.data:spring-data-commons') { + optionalApi('org.springframework.data:spring-data-commons') { exclude group: 'org.springframework' exclude group: 'io.micrometer' } - optional "com.jayway.jsonpath:json-path:$jaywayJsonPathVersion" + optionalApi "com.jayway.jsonpath:json-path:$jaywayJsonPathVersion" testImplementation "org.assertj:assertj-core:$assertjVersion" } @@ -433,22 +413,22 @@ project('spring-rabbit') { api 'org.springframework:spring-messaging' api 'org.springframework:spring-tx' api 'io.micrometer:micrometer-observation' - optional 'org.springframework:spring-aop' - optional 'org.springframework:spring-webflux' - optional "org.apache.httpcomponents.client5:httpclient5:$commonsHttpClientVersion" - optional 'io.projectreactor:reactor-core' - optional 'io.projectreactor.netty:reactor-netty-http' - optional "ch.qos.logback:logback-classic:$logbackVersion" - optional 'org.apache.logging.log4j:log4j-core' - optional 'io.micrometer:micrometer-core' - optional 'io.micrometer:micrometer-tracing' + optionalApi 'org.springframework:spring-aop' + optionalApi 'org.springframework:spring-webflux' + optionalApi "org.apache.httpcomponents.client5:httpclient5:$commonsHttpClientVersion" + optionalApi 'io.projectreactor:reactor-core' + optionalApi 'io.projectreactor.netty:reactor-netty-http' + optionalApi "ch.qos.logback:logback-classic:$logbackVersion" + optionalApi 'org.apache.logging.log4j:log4j-core' + optionalApi 'io.micrometer:micrometer-core' + optionalApi 'io.micrometer:micrometer-tracing' // Spring Data projection message binding support - optional("org.springframework.data:spring-data-commons") { + optionalApi("org.springframework.data:spring-data-commons") { exclude group: 'org.springframework' } - optional "com.jayway.jsonpath:json-path:$jaywayJsonPathVersion" - optional "org.apache.commons:commons-pool2:$commonsPoolVersion" - optional "org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlinCoroutinesVersion" + optionalApi "com.jayway.jsonpath:json-path:$jaywayJsonPathVersion" + optionalApi "org.apache.commons:commons-pool2:$commonsPoolVersion" + optionalApi "org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlinCoroutinesVersion" testApi project(':spring-rabbit-junit') testImplementation("com.willowtreeapps.assertk:assertk-jvm:$assertkVersion") @@ -482,7 +462,7 @@ project('spring-rabbit-stream') { dependencies { api project(':spring-rabbit') api "com.rabbitmq:stream-client:$rabbitmqStreamVersion" - optional 'io.micrometer:micrometer-core' + optionalApi 'io.micrometer:micrometer-core' testApi project(':spring-rabbit-junit') testRuntimeOnly 'com.fasterxml.jackson.core:jackson-core' @@ -514,13 +494,13 @@ project('spring-rabbit-junit') { api 'org.springframework:spring-web' api 'org.junit.jupiter:junit-jupiter-api' api "org.assertj:assertj-core:$assertjVersion" - optional("junit:junit:$junit4Version") { + optionalApi("junit:junit:$junit4Version") { exclude group: 'org.hamcrest', module: 'hamcrest-core' } - optional "org.testcontainers:rabbitmq" - optional "org.testcontainers:junit-jupiter" - optional "ch.qos.logback:logback-classic:$logbackVersion" - optional 'org.apache.logging.log4j:log4j-core' + optionalApi "org.testcontainers:rabbitmq" + optionalApi "org.testcontainers:junit-jupiter" + optionalApi "ch.qos.logback:logback-classic:$logbackVersion" + optionalApi 'org.apache.logging.log4j:log4j-core' compileOnly 'org.apiguardian:apiguardian-api:1.0.0' } } From 54e89efb4b6485a2a3301ee759d759428eca38a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 17 Aug 2024 02:59:40 +0000 Subject: [PATCH 515/737] Bump ch.qos.logback:logback-classic from 1.5.6 to 1.5.7 (#2789) Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.6 to 1.5.7. - [Commits](https://github.com/qos-ch/logback/compare/v_1.5.6...v_1.5.7) --- updated-dependencies: - dependency-name: ch.qos.logback:logback-classic dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 7d62748cd2..1b6b85c09e 100644 --- a/build.gradle +++ b/build.gradle @@ -59,7 +59,7 @@ ext { junitJupiterVersion = '5.11.0-RC1' kotlinCoroutinesVersion = '1.8.1' log4jVersion = '2.23.1' - logbackVersion = '1.5.6' + logbackVersion = '1.5.7' lz4Version = '1.8.0' micrometerDocsVersion = '1.0.3' micrometerVersion = '1.14.0-SNAPSHOT' From 8815e8a926d16343baab7af46503d84af5693581 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 17 Aug 2024 03:00:21 +0000 Subject: [PATCH 516/737] Bump org.springframework.data:spring-data-bom from 2024.0.2 to 2024.0.3 (#2791) Bumps [org.springframework.data:spring-data-bom](https://github.com/spring-projects/spring-data-bom) from 2024.0.2 to 2024.0.3. - [Release notes](https://github.com/spring-projects/spring-data-bom/releases) - [Commits](https://github.com/spring-projects/spring-data-bom/compare/2024.0.2...2024.0.3) --- updated-dependencies: - dependency-name: org.springframework.data:spring-data-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 1b6b85c09e..c4fb445e97 100644 --- a/build.gradle +++ b/build.gradle @@ -69,7 +69,7 @@ ext { rabbitmqVersion = '5.21.0' reactorVersion = '2024.0.0-SNAPSHOT' snappyVersion = '1.1.10.6' - springDataVersion = '2024.0.2' + springDataVersion = '2024.0.3' springRetryVersion = '2.0.7' springVersion = '6.2.0-SNAPSHOT' testcontainersVersion = '1.19.8' From 773e81460e3fe64df78f8675a64df112c9a8a7b7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 17 Aug 2024 03:00:27 +0000 Subject: [PATCH 517/737] Bump org.junit:junit-bom from 5.11.0-RC1 to 5.11.0 (#2790) Bumps [org.junit:junit-bom](https://github.com/junit-team/junit5) from 5.11.0-RC1 to 5.11.0. - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.11.0-RC1...r5.11.0) --- updated-dependencies: - dependency-name: org.junit:junit-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c4fb445e97..670f560f0f 100644 --- a/build.gradle +++ b/build.gradle @@ -56,7 +56,7 @@ ext { jacksonBomVersion = '2.17.2' jaywayJsonPathVersion = '2.9.0' junit4Version = '4.13.2' - junitJupiterVersion = '5.11.0-RC1' + junitJupiterVersion = '5.11.0' kotlinCoroutinesVersion = '1.8.1' log4jVersion = '2.23.1' logbackVersion = '1.5.7' From ff00be4d78817eb1cb7cdca187a6d0a550caddea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 17 Aug 2024 03:08:52 +0000 Subject: [PATCH 518/737] Bump org.springframework.retry:spring-retry from 2.0.7 to 2.0.8 (#2792) Bumps [org.springframework.retry:spring-retry](https://github.com/spring-projects/spring-retry) from 2.0.7 to 2.0.8. - [Release notes](https://github.com/spring-projects/spring-retry/releases) - [Commits](https://github.com/spring-projects/spring-retry/compare/v2.0.7...v2.0.8) --- updated-dependencies: - dependency-name: org.springframework.retry:spring-retry dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 670f560f0f..bc2d9ff84c 100644 --- a/build.gradle +++ b/build.gradle @@ -70,7 +70,7 @@ ext { reactorVersion = '2024.0.0-SNAPSHOT' snappyVersion = '1.1.10.6' springDataVersion = '2024.0.3' - springRetryVersion = '2.0.7' + springRetryVersion = '2.0.8' springVersion = '6.2.0-SNAPSHOT' testcontainersVersion = '1.19.8' zstdJniVersion = '1.5.6-4' From 46bf65a529e8e25322f4838250da94658f3ccefb Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 19 Aug 2024 16:15:02 -0400 Subject: [PATCH 519/737] Upgrade to the latest Milestones --- build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index bc2d9ff84c..52d178ea24 100644 --- a/build.gradle +++ b/build.gradle @@ -62,16 +62,16 @@ ext { logbackVersion = '1.5.7' lz4Version = '1.8.0' micrometerDocsVersion = '1.0.3' - micrometerVersion = '1.14.0-SNAPSHOT' - micrometerTracingVersion = '1.4.0-SNAPSHOT' + micrometerVersion = '1.14.0-M2' + micrometerTracingVersion = '1.4.0-M2' mockitoVersion = '5.12.0' rabbitmqStreamVersion = '0.15.0' rabbitmqVersion = '5.21.0' - reactorVersion = '2024.0.0-SNAPSHOT' + reactorVersion = '2024.0.0-M6' snappyVersion = '1.1.10.6' springDataVersion = '2024.0.3' springRetryVersion = '2.0.8' - springVersion = '6.2.0-SNAPSHOT' + springVersion = '6.2.0-M7' testcontainersVersion = '1.19.8' zstdJniVersion = '1.5.6-4' From b92a7ade4eca6fdad1fb0179ba875a4b8ee8c75a Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 19 Aug 2024 16:16:55 -0400 Subject: [PATCH 520/737] Fix Reactor version to `2024.0.0-M5` --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 52d178ea24..2ee2fcbe8e 100644 --- a/build.gradle +++ b/build.gradle @@ -67,7 +67,7 @@ ext { mockitoVersion = '5.12.0' rabbitmqStreamVersion = '0.15.0' rabbitmqVersion = '5.21.0' - reactorVersion = '2024.0.0-M6' + reactorVersion = '2024.0.0-M5' snappyVersion = '1.1.10.6' springDataVersion = '2024.0.3' springRetryVersion = '2.0.8' From 5d53abd56b99a4af211d3107d0717975e42acb3a Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 19 Aug 2024 20:23:01 +0000 Subject: [PATCH 521/737] [artifactory-release] Release version 3.2.0-M2 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 6bc0422ab1..70e4028a12 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.2.0-SNAPSHOT +version=3.2.0-M2 org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true From 746f5f65969f2f3479e39ae4530d01d7309ef543 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 19 Aug 2024 20:23:06 +0000 Subject: [PATCH 522/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 70e4028a12..6bc0422ab1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.2.0-M2 +version=3.2.0-SNAPSHOT org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true From b846631f7d66fb987d1f478d46dec23f10885dd8 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 21 Aug 2024 13:30:59 -0400 Subject: [PATCH 523/737] Add `announce-milestone-planning.yml` GHA Send a `planning` message to the chat when `due_on` is changed on a milestone --- .github/workflows/announce-milestone-planning.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .github/workflows/announce-milestone-planning.yml diff --git a/.github/workflows/announce-milestone-planning.yml b/.github/workflows/announce-milestone-planning.yml new file mode 100644 index 0000000000..e4e90710c9 --- /dev/null +++ b/.github/workflows/announce-milestone-planning.yml @@ -0,0 +1,11 @@ +name: Announce Milestone Planning in Chat + +on: + milestone: + types: [created, edited] + +jobs: + announce-milestone-planning: + uses: spring-io/spring-github-workflows/.github/workflows/spring-announce-milestone-planning.yml@main + secrets: + SPRING_RELEASE_CHAT_WEBHOOK_URL: ${{ secrets.SPRING_RELEASE_GCHAT_WEBHOOK_URL }} \ No newline at end of file From 0de6c5a15cab68eac977c1e59fdfbc119e7a1dd6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 24 Aug 2024 02:12:37 +0000 Subject: [PATCH 524/737] Bump io.spring.develocity.conventions (#2794) Bumps the development-dependencies group with 1 update: [io.spring.develocity.conventions](https://github.com/spring-io/develocity-conventions). Updates `io.spring.develocity.conventions` from 0.0.19 to 0.0.20 - [Release notes](https://github.com/spring-io/develocity-conventions/releases) - [Commits](https://github.com/spring-io/develocity-conventions/compare/v0.0.19...v0.0.20) --- updated-dependencies: - dependency-name: io.spring.develocity.conventions dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- settings.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index 20c7b45f6e..4e1e2619ab 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,7 +7,7 @@ pluginManagement { plugins { id 'com.gradle.develocity' version '3.17.6' - id 'io.spring.develocity.conventions' version '0.0.19' + id 'io.spring.develocity.conventions' version '0.0.20' } rootProject.name = 'spring-amqp-dist' From 939e3929161f38e479409d4acab34c402ceaac28 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 24 Aug 2024 02:13:00 +0000 Subject: [PATCH 525/737] Bump io.micrometer:micrometer-bom from 1.14.0-M2 to 1.14.0-SNAPSHOT (#2796) Bumps [io.micrometer:micrometer-bom](https://github.com/micrometer-metrics/micrometer) from 1.14.0-M2 to 1.14.0-SNAPSHOT. - [Release notes](https://github.com/micrometer-metrics/micrometer/releases) - [Commits](https://github.com/micrometer-metrics/micrometer/commits) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2ee2fcbe8e..846de84295 100644 --- a/build.gradle +++ b/build.gradle @@ -62,7 +62,7 @@ ext { logbackVersion = '1.5.7' lz4Version = '1.8.0' micrometerDocsVersion = '1.0.3' - micrometerVersion = '1.14.0-M2' + micrometerVersion = '1.14.0-SNAPSHOT' micrometerTracingVersion = '1.4.0-M2' mockitoVersion = '5.12.0' rabbitmqStreamVersion = '0.15.0' From 8fed8b9ff6adee53a736f020777b35dfdb6aaa63 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 24 Aug 2024 02:14:12 +0000 Subject: [PATCH 526/737] Bump io.projectreactor:reactor-bom from 2024.0.0-M5 to 2024.0.0-SNAPSHOT (#2795) Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2024.0.0-M5 to 2024.0.0-SNAPSHOT. - [Release notes](https://github.com/reactor/reactor/releases) - [Commits](https://github.com/reactor/reactor/commits) --- updated-dependencies: - dependency-name: io.projectreactor:reactor-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 846de84295..fc302972fd 100644 --- a/build.gradle +++ b/build.gradle @@ -67,7 +67,7 @@ ext { mockitoVersion = '5.12.0' rabbitmqStreamVersion = '0.15.0' rabbitmqVersion = '5.21.0' - reactorVersion = '2024.0.0-M5' + reactorVersion = '2024.0.0-SNAPSHOT' snappyVersion = '1.1.10.6' springDataVersion = '2024.0.3' springRetryVersion = '2.0.8' From e1b85f93081f2212db278e6194dae9191084db05 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 24 Aug 2024 02:15:17 +0000 Subject: [PATCH 527/737] Bump org.springframework:spring-framework-bom (#2797) Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.2.0-M7 to 6.2.0-SNAPSHOT. - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/commits) --- updated-dependencies: - dependency-name: org.springframework:spring-framework-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index fc302972fd..786a87abb9 100644 --- a/build.gradle +++ b/build.gradle @@ -71,7 +71,7 @@ ext { snappyVersion = '1.1.10.6' springDataVersion = '2024.0.3' springRetryVersion = '2.0.8' - springVersion = '6.2.0-M7' + springVersion = '6.2.0-SNAPSHOT' testcontainersVersion = '1.19.8' zstdJniVersion = '1.5.6-4' From ea9aab933952debf759c7ba207c15a63acea28a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 24 Aug 2024 02:16:40 +0000 Subject: [PATCH 528/737] Bump io.micrometer:micrometer-tracing-bom (#2798) Bumps [io.micrometer:micrometer-tracing-bom](https://github.com/micrometer-metrics/tracing) from 1.4.0-M2 to 1.4.0-SNAPSHOT. - [Release notes](https://github.com/micrometer-metrics/tracing/releases) - [Commits](https://github.com/micrometer-metrics/tracing/commits) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-tracing-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 786a87abb9..bc2d9ff84c 100644 --- a/build.gradle +++ b/build.gradle @@ -63,7 +63,7 @@ ext { lz4Version = '1.8.0' micrometerDocsVersion = '1.0.3' micrometerVersion = '1.14.0-SNAPSHOT' - micrometerTracingVersion = '1.4.0-M2' + micrometerTracingVersion = '1.4.0-SNAPSHOT' mockitoVersion = '5.12.0' rabbitmqStreamVersion = '0.15.0' rabbitmqVersion = '5.21.0' From 6841624cd08d83c13209269edbf663a03f773c2b Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 28 Aug 2024 09:13:33 -0400 Subject: [PATCH 529/737] Remove redundant `OSSRH_URL` secret for release workflow **Auto-cherry-pick to `3.1.x`** --- .github/workflows/release.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3400ec5ba1..c0bea04ccc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,14 +12,13 @@ jobs: contents: write issues: write - uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-release.yml@v3 + uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-release.yml@main secrets: GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} GRADLE_ENTERPRISE_SECRET_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} JF_ARTIFACTORY_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} - OSSRH_URL: ${{ secrets.OSSRH_URL }} OSSRH_S01_TOKEN_USERNAME: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }} OSSRH_S01_TOKEN_PASSWORD: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }} OSSRH_STAGING_PROFILE_NAME: ${{ secrets.OSSRH_STAGING_PROFILE_NAME }} From da4a7a6b0892b44c3bcdd46f747ce50c916ac793 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 31 Aug 2024 02:42:37 +0000 Subject: [PATCH 530/737] Bump com.github.luben:zstd-jni from 1.5.6-4 to 1.5.6-5 (#2802) Bumps [com.github.luben:zstd-jni](https://github.com/luben/zstd-jni) from 1.5.6-4 to 1.5.6-5. - [Commits](https://github.com/luben/zstd-jni/compare/v1.5.6-4...v1.5.6-5) --- updated-dependencies: - dependency-name: com.github.luben:zstd-jni dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index bc2d9ff84c..54b1967554 100644 --- a/build.gradle +++ b/build.gradle @@ -73,7 +73,7 @@ ext { springRetryVersion = '2.0.8' springVersion = '6.2.0-SNAPSHOT' testcontainersVersion = '1.19.8' - zstdJniVersion = '1.5.6-4' + zstdJniVersion = '1.5.6-5' javaProjects = subprojects - project(':spring-amqp-bom') } From 1f234cd0ff3a0166c8c69e38cc3f7b3167a90307 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 31 Aug 2024 02:42:54 +0000 Subject: [PATCH 531/737] Bump com.github.spotbugs in the development-dependencies group (#2801) Bumps the development-dependencies group with 1 update: com.github.spotbugs. Updates `com.github.spotbugs` from 6.0.20 to 6.0.21 --- updated-dependencies: - dependency-name: com.github.spotbugs dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 54b1967554..b7eb6a7dc7 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ plugins { id 'io.spring.dependency-management' version '1.1.6' apply false id 'org.antora' version '1.0.0' id 'io.spring.antora.generate-antora-yml' version '0.0.1' - id 'com.github.spotbugs' version '6.0.20' + id 'com.github.spotbugs' version '6.0.21' id 'io.freefair.aggregate-javadoc' version '8.6' } From 8f8e1d1294389443074e2a52f5df9a307d24bf3c Mon Sep 17 00:00:00 2001 From: Thomas Badie Date: Tue, 3 Sep 2024 15:00:53 +0100 Subject: [PATCH 532/737] GH-2805: Add a checkAfterCompletion to SimpleMessageListenerContainer Fixes: #2805 Issue link: https://github.com/spring-projects/spring-amqp/issues/2805 Run `checkAfterCompletion` to the `SimpleMessageListenerContainer`. That allows to be notified when there is an issue with the commit. --- .../SimpleMessageListenerContainer.java | 4 + .../listener/ExternalTxManagerSMLCTests.java | 122 +++++++++++++++++- .../listener/ExternalTxManagerTests.java | 31 ++++- 3 files changed, 153 insertions(+), 4 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java index 5e4ae9ce22..f4f41b4de6 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java @@ -83,6 +83,7 @@ * @author Tim Bourquin * @author Jeonggi Kim * @author Java4ye + * @author Thomas Badie * * @since 1.0 */ @@ -1013,6 +1014,9 @@ private boolean receiveAndExecute(final BlockingQueueConsumer consumer) throws E catch (WrappedTransactionException e) { // NOSONAR exception flow control throw (Exception) e.getCause(); } + finally { + ConnectionFactoryUtils.checkAfterCompletion(); + } } return doReceiveAndExecute(consumer); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ExternalTxManagerSMLCTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ExternalTxManagerSMLCTests.java index e1fb3e6941..44350d83e0 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ExternalTxManagerSMLCTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ExternalTxManagerSMLCTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2024 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. @@ -16,10 +16,42 @@ package org.springframework.amqp.rabbit.listener; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + import org.springframework.amqp.rabbit.connection.AbstractConnectionFactory; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactoryUtils; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.context.ApplicationEventPublisher; + +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.Consumer; +import com.rabbitmq.client.Envelope; /** * @author Gary Russell + * @author Thomas Badie * @since 2.0 * */ @@ -32,4 +64,92 @@ protected AbstractMessageListenerContainer createContainer(AbstractConnectionFac return container; } + + @Test + public void testMessageListenerTxFail() throws Exception { + ConnectionFactoryUtils.enableAfterCompletionFailureCapture(true); + ConnectionFactory mockConnectionFactory = mock(ConnectionFactory.class); + Connection mockConnection = mock(Connection.class); + final Channel mockChannel = mock(Channel.class); + given(mockChannel.isOpen()).willReturn(true); + given(mockChannel.txSelect()).willReturn(mock(AMQP.Tx.SelectOk.class)); + final AtomicReference commitLatch = new AtomicReference<>(new CountDownLatch(1)); + String exceptionMessage = "Failed to commit."; + willAnswer(invocation -> { + commitLatch.get().countDown(); + throw new IllegalStateException(exceptionMessage); + }).given(mockChannel).txCommit(); + + final CachingConnectionFactory cachingConnectionFactory = new CachingConnectionFactory(mockConnectionFactory); + cachingConnectionFactory.setExecutor(mock(ExecutorService.class)); + given(mockConnectionFactory.newConnection(any(ExecutorService.class), anyString())).willReturn(mockConnection); + given(mockConnection.isOpen()).willReturn(true); + + willAnswer(invocation -> mockChannel).given(mockConnection).createChannel(); + + final AtomicReference consumer = new AtomicReference(); + final CountDownLatch consumerLatch = new CountDownLatch(1); + + willAnswer(invocation -> { + consumer.set(invocation.getArgument(6)); + consumerLatch.countDown(); + return "consumerTag"; + }).given(mockChannel) + .basicConsume(anyString(), anyBoolean(), anyString(), anyBoolean(), anyBoolean(), anyMap(), + any(Consumer.class)); + + + final CountDownLatch latch = new CountDownLatch(1); + AbstractMessageListenerContainer container = createContainer(cachingConnectionFactory); + container.setMessageListener(message -> { + RabbitTemplate rabbitTemplate = new RabbitTemplate(cachingConnectionFactory); + rabbitTemplate.setChannelTransacted(true); + // should use same channel as container + rabbitTemplate.convertAndSend("foo", "bar", "baz"); + latch.countDown(); + }); + container.setQueueNames("queue"); + container.setChannelTransacted(true); + container.setShutdownTimeout(100); + DummyTxManager transactionManager = new DummyTxManager(); + container.setTransactionManager(transactionManager); + ApplicationEventPublisher applicationEventPublisher = mock(ApplicationEventPublisher.class); + final CountDownLatch applicationEventPublisherLatch = new CountDownLatch(1); + willAnswer(invocation -> { + if (invocation.getArgument(0) instanceof ListenerContainerConsumerFailedEvent) { + applicationEventPublisherLatch.countDown(); + } + return null; + }).given(applicationEventPublisher).publishEvent(any()); + + container.setApplicationEventPublisher(applicationEventPublisher); + container.afterPropertiesSet(); + container.start(); + assertThat(consumerLatch.await(10, TimeUnit.SECONDS)).isTrue(); + + consumer.get().handleDelivery("qux", + new Envelope(1, false, "foo", "bar"), new AMQP.BasicProperties(), + new byte[] { 0 }); + + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + + verify(mockConnection, times(1)).createChannel(); + assertThat(commitLatch.get().await(10, TimeUnit.SECONDS)).isTrue(); + verify(mockChannel).basicAck(anyLong(), anyBoolean()); + verify(mockChannel).txCommit(); + + assertThat(applicationEventPublisherLatch.await(10, TimeUnit.SECONDS)).isTrue(); + verify(applicationEventPublisher).publishEvent(any(ListenerContainerConsumerFailedEvent.class)); + + ArgumentCaptor argumentCaptor + = ArgumentCaptor.forClass(ListenerContainerConsumerFailedEvent.class); + verify(applicationEventPublisher).publishEvent(argumentCaptor.capture()); + assertThat(argumentCaptor.getValue().getThrowable()).hasCauseInstanceOf(IllegalStateException.class); + assertThat(argumentCaptor.getValue().getThrowable()) + .isNotNull().extracting(Throwable::getCause) + .isNotNull().extracting(Throwable::getMessage).isEqualTo(exceptionMessage); + container.stop(); + } + + } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ExternalTxManagerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ExternalTxManagerTests.java index ac4e0c86bc..dedb34a915 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ExternalTxManagerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ExternalTxManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -69,6 +69,7 @@ /** * @author Gary Russell + * @author Thomas Badie * @since 1.1.2 * */ @@ -758,7 +759,7 @@ public void testMessageListenerWithRabbitTxManager() throws Exception { container.stop(); } - private Answer ensureOneChannelAnswer(final Channel onlyChannel, + protected Answer ensureOneChannelAnswer(final Channel onlyChannel, final AtomicReference tooManyChannels) { final AtomicBoolean done = new AtomicBoolean(); return invocation -> { @@ -776,7 +777,7 @@ private Answer ensureOneChannelAnswer(final Channel onlyChannel, protected abstract AbstractMessageListenerContainer createContainer(AbstractConnectionFactory connectionFactory); @SuppressWarnings("serial") - private static class DummyTxManager extends AbstractPlatformTransactionManager { + protected static class DummyTxManager extends AbstractPlatformTransactionManager { private volatile boolean committed; @@ -804,6 +805,30 @@ protected void doRollback(DefaultTransactionStatus status) throws TransactionExc this.rolledBack = true; this.latch.countDown(); } + + public boolean isCommitted() { + return committed; + } + + public void setCommitted(boolean committed) { + this.committed = committed; + } + + public boolean isRolledBack() { + return rolledBack; + } + + public void setRolledBack(boolean rolledBack) { + this.rolledBack = rolledBack; + } + + public CountDownLatch getLatch() { + return latch; + } + + public void setLatch(CountDownLatch latch) { + this.latch = latch; + } } } From d7058bbac660741bbd84cd14904683fc65696015 Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Wed, 4 Sep 2024 23:20:44 +0700 Subject: [PATCH 533/737] Apply Java pattern matching --- .../amqp/core/MessageProperties.java | 8 ++-- .../support/AmqpMessageHeaderAccessor.java | 10 ++--- .../amqp/support/SimpleAmqpHeaderMapper.java | 8 ++-- .../AbstractJackson2MessageConverter.java | 10 ++--- ...nvocationAwareMessageConverterAdapter.java | 6 +-- .../converter/RemoteInvocationResult.java | 6 +-- .../converter/SerializerMessageConverter.java | 8 ++-- .../converter/SimpleMessageConverter.java | 8 ++-- .../AbstractDecompressingPostProcessor.java | 4 +- ...istDeserializingMessageConverterTests.java | 4 +- .../StreamRabbitListenerContainerFactory.java | 6 +-- .../listener/StreamListenerContainer.java | 8 ++-- .../DefaultStreamMessageConverter.java | 42 +++++++++---------- .../amqp/rabbit/test/TestRabbitTemplate.java | 13 +++--- .../BaseRabbitListenerContainerFactory.java | 5 +-- ...RetryOperationsInterceptorFactoryBean.java | 10 ++--- .../PooledChannelConnectionFactory.java | 6 +-- .../amqp/rabbit/connection/RabbitUtils.java | 8 ++-- ...DirectReplyToMessageListenerContainer.java | 4 +- .../DefaultMessagePropertiesConverter.java | 6 +-- .../support/RabbitExceptionTranslator.java | 10 ++--- .../EnableRabbitIntegrationTests.java | 10 ++--- ...tenerContainerFactoryIntegrationTests.java | 6 +-- ...BlockingQueueConsumerIntegrationTests.java | 6 +-- ...ContainerErrorHandlerIntegrationTests.java | 4 +- ...ageListenerContainerIntegration2Tests.java | 4 +- 26 files changed, 107 insertions(+), 113 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java index 2aa516d7db..efb2af4b61 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java @@ -447,12 +447,10 @@ public void setConsumerQueue(String consumerQueue) { */ public Long getDelayLong() { Object delay = this.headers.get(X_DELAY); - if (delay instanceof Long) { - return (Long) delay; - } - else { - return null; + if (delay instanceof Long delayLong) { + return delayLong; } + return null; } /** diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/AmqpMessageHeaderAccessor.java b/spring-amqp/src/main/java/org/springframework/amqp/support/AmqpMessageHeaderAccessor.java index 09ca52a90b..b6472364a1 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/AmqpMessageHeaderAccessor.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/AmqpMessageHeaderAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2024 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. @@ -87,12 +87,10 @@ public Long getContentLength() { @Override public MimeType getContentType() { Object value = getHeader(AmqpHeaders.CONTENT_TYPE); - if (value instanceof String) { - return MimeType.valueOf((String) value); - } - else { - return super.getContentType(); + if (value instanceof String contentType) { + return MimeType.valueOf(contentType); } + return super.getContentType(); } public String getCorrelationId() { diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/SimpleAmqpHeaderMapper.java b/spring-amqp/src/main/java/org/springframework/amqp/support/SimpleAmqpHeaderMapper.java index 51af201fa8..2c9a35864b 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/SimpleAmqpHeaderMapper.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/SimpleAmqpHeaderMapper.java @@ -66,8 +66,8 @@ public void fromHeaders(MessageHeaders headers, MessageProperties amqpMessagePro amqpMessageProperties::setContentLength) .acceptIfHasText(extractContentTypeAsString(headers), amqpMessageProperties::setContentType); Object correlationId = headers.get(AmqpHeaders.CORRELATION_ID); - if (correlationId instanceof String) { - amqpMessageProperties.setCorrelationId((String) correlationId); + if (correlationId instanceof String string) { + amqpMessageProperties.setCorrelationId(string); } javaUtils .acceptIfNotNull(getHeaderIfAvailable(headers, AmqpHeaders.DELAY, Long.class), @@ -195,8 +195,8 @@ private String extractContentTypeAsString(Map headers) { if (contentType instanceof MimeType) { contentTypeStringValue = contentType.toString(); } - else if (contentType instanceof String) { - contentTypeStringValue = (String) contentType; + else if (contentType instanceof String string) { + contentTypeStringValue = string; } else { if (logger.isWarnEnabled()) { diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java index 14f07da963..e9cb03ab6d 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2022 the original author or authors. + * Copyright 2018-2024 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. @@ -246,8 +246,8 @@ public void setTypePrecedence(Jackson2JavaTypeMapper.TypePrecedence typePreceden if (this.typeMapperSet) { throw new IllegalStateException("When providing your own type mapper, you should set the precedence on it"); } - if (this.javaTypeMapper instanceof DefaultJackson2JavaTypeMapper) { - ((DefaultJackson2JavaTypeMapper) this.javaTypeMapper).setTypePrecedence(typePrecedence); + if (this.javaTypeMapper instanceof DefaultJackson2JavaTypeMapper defaultJackson2JavaTypeMapper) { + defaultJackson2JavaTypeMapper.setTypePrecedence(typePrecedence); } else { throw new IllegalStateException("Type precedence is available with the DefaultJackson2JavaTypeMapper"); @@ -384,10 +384,10 @@ else if (inferredType != null && this.alwaysConvertToInferredType) { content = tryConverType(message, encoding, inferredType); } if (content == null) { - if (conversionHint instanceof ParameterizedTypeReference) { + if (conversionHint instanceof ParameterizedTypeReference parameterizedTypeReference) { content = convertBytesToObject(message.getBody(), encoding, this.objectMapper.getTypeFactory().constructType( - ((ParameterizedTypeReference) conversionHint).getType())); + parameterizedTypeReference.getType())); } else if (getClassMapper() == null) { JavaType targetJavaType = getJavaTypeMapper() diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationAwareMessageConverterAdapter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationAwareMessageConverterAdapter.java index 61fe092e68..2bb40fa8ae 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationAwareMessageConverterAdapter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationAwareMessageConverterAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 the original author or authors. + * Copyright 2016-2024 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. @@ -51,9 +51,9 @@ public Message toMessage(Object object, MessageProperties messageProperties) thr @Override public Object fromMessage(Message message) throws MessageConversionException { Object result = this.delegate.fromMessage(message); - if (result instanceof RemoteInvocationResult) { + if (result instanceof RemoteInvocationResult remoteInvocationResult) { try { - result = ((RemoteInvocationResult) result).recreate(); + result = remoteInvocationResult.recreate(); if (result == null) { throw new MessageConversionException("RemoteInvocationResult returned null"); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationResult.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationResult.java index aba8346e01..7d08e1d139 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationResult.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationResult.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2024 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. @@ -143,8 +143,8 @@ public boolean hasInvocationTargetException() { public Object recreate() throws Throwable { if (this.exception != null) { Throwable exToThrow = this.exception; - if (this.exception instanceof InvocationTargetException) { - exToThrow = ((InvocationTargetException) this.exception).getTargetException(); + if (this.exception instanceof InvocationTargetException invocationTargetException) { + exToThrow = invocationTargetException.getTargetException(); } RemoteInvocationUtils.fillInClientStackTraceIfPossible(exToThrow); throw exToThrow; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SerializerMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SerializerMessageConverter.java index cfe8e923d5..fdfc365c0e 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SerializerMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SerializerMessageConverter.java @@ -187,9 +187,9 @@ protected Message createMessage(Object object, MessageProperties messageProperti throws MessageConversionException { byte[] bytes; - if (object instanceof String) { + if (object instanceof String string) { try { - bytes = ((String) object).getBytes(this.defaultCharset); + bytes = string.getBytes(this.defaultCharset); } catch (UnsupportedEncodingException e) { throw new MessageConversionException("failed to convert Message content", e); @@ -197,8 +197,8 @@ protected Message createMessage(Object object, MessageProperties messageProperti messageProperties.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN); messageProperties.setContentEncoding(this.defaultCharset); } - else if (object instanceof byte[]) { - bytes = (byte[]) object; + else if (object instanceof byte[] objectBytes) { + bytes = objectBytes; messageProperties.setContentType(MessageProperties.CONTENT_TYPE_BYTES); } else { diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SimpleMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SimpleMessageConverter.java index 56897f80fb..77ac469f21 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SimpleMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SimpleMessageConverter.java @@ -112,13 +112,13 @@ protected Message createMessage(Object object, MessageProperties messageProperti throws MessageConversionException { byte[] bytes = null; - if (object instanceof byte[]) { - bytes = (byte[]) object; + if (object instanceof byte[] objectBytes) { + bytes = objectBytes; messageProperties.setContentType(MessageProperties.CONTENT_TYPE_BYTES); } - else if (object instanceof String) { + else if (object instanceof String string) { try { - bytes = ((String) object).getBytes(this.defaultCharset); + bytes = string.getBytes(this.defaultCharset); } catch (UnsupportedEncodingException e) { throw new MessageConversionException("failed to convert to Message content", e); diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/AbstractDecompressingPostProcessor.java b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/AbstractDecompressingPostProcessor.java index a8274c11ae..249f0e25e4 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/AbstractDecompressingPostProcessor.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/AbstractDecompressingPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * Copyright 2014-2024 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. @@ -83,7 +83,7 @@ protected void setOrder(int order) { public Message postProcessMessage(Message message) throws AmqpException { Object autoDecompress = message.getMessageProperties().getHeaders() .get(MessageProperties.SPRING_AUTO_DECOMPRESS); - if (this.alwaysDecompress || (autoDecompress instanceof Boolean && ((Boolean) autoDecompress))) { + if (this.alwaysDecompress || (autoDecompress instanceof Boolean isAutoDecompress && isAutoDecompress)) { ByteArrayInputStream zipped = new ByteArrayInputStream(message.getBody()); try { InputStream unzipper = getDecompressorStream(zipped); diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/AllowedListDeserializingMessageConverterTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/AllowedListDeserializingMessageConverterTests.java index e56a568a16..573784c1b5 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/AllowedListDeserializingMessageConverterTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/AllowedListDeserializingMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 the original author or authors. + * Copyright 2016-2024 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. @@ -74,7 +74,7 @@ protected TestBean(String text) { @Override public boolean equals(Object other) { - return (other instanceof TestBean && this.text.equals(((TestBean) other).text)); + return (other instanceof TestBean testBean && this.text.equals(testBean.text)); } @Override diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/StreamRabbitListenerContainerFactory.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/StreamRabbitListenerContainerFactory.java index 7b8aa73a2d..a589f6ffd1 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/StreamRabbitListenerContainerFactory.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/StreamRabbitListenerContainerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors. + * Copyright 2021-2024 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. @@ -104,8 +104,8 @@ public void setStreamListenerObservationConvention( @Override public StreamListenerContainer createListenerContainer(RabbitListenerEndpoint endpoint) { - if (endpoint instanceof MethodRabbitListenerEndpoint && this.nativeListener) { - ((MethodRabbitListenerEndpoint) endpoint).setAdapterProvider( + if (endpoint instanceof MethodRabbitListenerEndpoint methodRabbitListenerEndpoint && this.nativeListener) { + methodRabbitListenerEndpoint.setAdapterProvider( (boolean batch, Object bean, Method method, boolean returnExceptions, RabbitListenerErrorHandler errorHandler, @Nullable BatchingStrategy batchingStrategy) -> { diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java index 61775064dc..925565cb29 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java @@ -338,11 +338,11 @@ public void setupMessageListener(MessageListener messageListener) { } else { Message message2 = this.streamConverter.toMessage(message, new StreamMessageProperties(context)); - if (this.messageListener instanceof ChannelAwareMessageListener) { + if (this.messageListener instanceof ChannelAwareMessageListener channelAwareMessageListener) { try { observation.observe(() -> { try { - ((ChannelAwareMessageListener) this.messageListener).onMessage(message2, null); + channelAwareMessageListener.onMessage(message2, null); if (finalSample != null) { micrometerHolder.success(finalSample, this.streamName); } @@ -375,8 +375,8 @@ public void setupMessageListener(MessageListener messageListener) { private void adviseIfNeeded(MessageListener messageListener) { this.messageListener = messageListener; - if (messageListener instanceof StreamMessageListener) { - this.streamListener = (StreamMessageListener) messageListener; + if (messageListener instanceof StreamMessageListener streamMessageListener) { + this.streamListener = streamMessageListener; } if (this.adviceChain != null && this.adviceChain.length > 0) { ProxyFactory factory = new ProxyFactory(messageListener); diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverter.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverter.java index ac5664b059..dd2d765fd7 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverter.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2024 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. @@ -115,35 +115,35 @@ public com.rabbitmq.stream.Message fromMessage(Message message) throws MessageCo } private void mapProp(String key, Object val, ApplicationPropertiesBuilder builder) { // NOSONAR - complexity - if (val instanceof String) { - builder.entry(key, (String) val); + if (val instanceof String string) { + builder.entry(key, string); } - else if (val instanceof Long) { - builder.entry(key, (Long) val); + else if (val instanceof Long longValue) { + builder.entry(key, longValue); } - else if (val instanceof Integer) { - builder.entry(key, (Integer) val); + else if (val instanceof Integer intValue) { + builder.entry(key, intValue); } - else if (val instanceof Short) { - builder.entry(key, (Short) val); + else if (val instanceof Short shortValue) { + builder.entry(key, shortValue); } - else if (val instanceof Byte) { - builder.entry(key, (Byte) val); + else if (val instanceof Byte byteValue) { + builder.entry(key, byteValue); } - else if (val instanceof Double) { - builder.entry(key, (Double) val); + else if (val instanceof Double doubleValue) { + builder.entry(key, doubleValue); } - else if (val instanceof Float) { - builder.entry(key, (Float) val); + else if (val instanceof Float floatValue) { + builder.entry(key, floatValue); } - else if (val instanceof Character) { - builder.entry(key, (Character) val); + else if (val instanceof Character character) { + builder.entry(key, character); } - else if (val instanceof UUID) { - builder.entry(key, (UUID) val); + else if (val instanceof UUID uuid) { + builder.entry(key, uuid); } - else if (val instanceof byte[]) { - builder.entry(key, (byte[]) val); + else if (val instanceof byte[] bytes) { + builder.entry(key, bytes); } } diff --git a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/TestRabbitTemplate.java b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/TestRabbitTemplate.java index c2c74b9405..dd35f91a41 100644 --- a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/TestRabbitTemplate.java +++ b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/TestRabbitTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-2024 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. @@ -143,9 +143,8 @@ protected Message doSendAndReceiveWithFixed(String exchange, String routingKey, Channel channel = mock(Channel.class); final AtomicReference reply = new AtomicReference<>(); Object listener = listenersForRoute.next(); - if (listener instanceof AbstractAdaptableMessageListener) { + if (listener instanceof AbstractAdaptableMessageListener adapter) { try { - AbstractAdaptableMessageListener adapter = (AbstractAdaptableMessageListener) listener; willAnswer(i -> { Envelope envelope = new Envelope(1, false, "", REPLY_QUEUE); reply.set(MessageBuilder.withBody(i.getArgument(4)) // NOSONAR magic # @@ -170,16 +169,16 @@ protected Message doSendAndReceiveWithFixed(String exchange, String routingKey, } private void invoke(Object listener, Message message, Channel channel) { - if (listener instanceof ChannelAwareMessageListener) { + if (listener instanceof ChannelAwareMessageListener channelAwareMessageListener) { try { - ((ChannelAwareMessageListener) listener).onMessage(message, channel); + channelAwareMessageListener.onMessage(message, channel); } catch (Exception e) { throw RabbitExceptionTranslator.convertRabbitAccessException(e); } } - else if (listener instanceof MessageListener) { - ((MessageListener) listener).onMessage(message); + else if (listener instanceof MessageListener messageListener) { + messageListener.onMessage(message); } else { // Not really necessary since the container doesn't allow it, but no hurt diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BaseRabbitListenerContainerFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BaseRabbitListenerContainerFactory.java index 46aee04253..5a1fab024a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BaseRabbitListenerContainerFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BaseRabbitListenerContainerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors. + * Copyright 2021-2024 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. @@ -140,8 +140,7 @@ protected void applyCommonOverrides(@Nullable RabbitListenerEndpoint endpoint, C endpoint.setupListenerContainer(instance); } Object iml = instance.getMessageListener(); - if (iml instanceof AbstractAdaptableMessageListener) { - AbstractAdaptableMessageListener messageListener = (AbstractAdaptableMessageListener) iml; + if (iml instanceof AbstractAdaptableMessageListener messageListener) { JavaUtils.INSTANCE // NOSONAR .acceptIfNotNull(this.beforeSendReplyPostProcessors, messageListener::setBeforeSendReplyPostProcessors) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatelessRetryOperationsInterceptorFactoryBean.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatelessRetryOperationsInterceptorFactoryBean.java index bb69a0b374..17288ec683 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatelessRetryOperationsInterceptorFactoryBean.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatelessRetryOperationsInterceptorFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -74,11 +74,11 @@ protected Object recover(Object[] args, Throwable cause) { if (messageRecoverer == null) { this.logger.warn("Message(s) dropped on recovery: " + arg, cause); } - else if (arg instanceof Message) { - messageRecoverer.recover((Message) arg, cause); + else if (arg instanceof Message message) { + messageRecoverer.recover(message, cause); } - else if (arg instanceof List && messageRecoverer instanceof MessageBatchRecoverer) { - ((MessageBatchRecoverer) messageRecoverer).recover((List) arg, cause); + else if (arg instanceof List && messageRecoverer instanceof MessageBatchRecoverer messageBatchRecoverer) { + messageBatchRecoverer.recover((List) arg, cause); } return null; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java index b2072f31f9..a458be72ab 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2024 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. @@ -343,8 +343,8 @@ public PooledObject makeObject() { @Override public void destroyObject(PooledObject p) throws Exception { Channel channel = p.getObject(); - if (channel instanceof ChannelProxy) { - channel = ((ChannelProxy) channel).getTargetChannel(); + if (channel instanceof ChannelProxy channelProxy) { + channel = channelProxy.getTargetChannel(); } ConnectionWrapper.this.physicalClose(channel); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java index 1a055fed30..4a958f6529 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java @@ -314,8 +314,8 @@ public static boolean isMismatchedQueueArgs(Exception e) { Throwable cause = e; ShutdownSignalException sig = null; while (cause != null && sig == null) { - if (cause instanceof ShutdownSignalException) { - sig = (ShutdownSignalException) cause; + if (cause instanceof ShutdownSignalException shutdownSignalException) { + sig = shutdownSignalException; } cause = cause.getCause(); } @@ -344,8 +344,8 @@ public static boolean isExchangeDeclarationFailure(Exception e) { Throwable cause = e; ShutdownSignalException sig = null; while (cause != null && sig == null) { - if (cause instanceof ShutdownSignalException) { - sig = (ShutdownSignalException) cause; + if (cause instanceof ShutdownSignalException shutdownSignalException) { + sig = shutdownSignalException; } cause = cause.getCause(); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainer.java index 6a777b0865..d4dc2278b7 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainer.java @@ -86,10 +86,10 @@ public final boolean removeQueueNames(String... queueNames) { @Override public void setMessageListener(MessageListener messageListener) { - if (messageListener instanceof ChannelAwareMessageListener) { + if (messageListener instanceof ChannelAwareMessageListener channelAwareMessageListener) { super.setMessageListener((ChannelAwareMessageListener) (message, channel) -> { try { - ((ChannelAwareMessageListener) messageListener).onMessage(message, channel); + channelAwareMessageListener.onMessage(message, channel); } finally { this.inUseConsumerChannels.remove(channel); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java index 7684aab004..489107b7aa 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java @@ -203,9 +203,9 @@ else if (value instanceof Object[] array) { } value = writableArray; } - else if (value instanceof List) { - List writableList = new ArrayList<>(((List) value).size()); - for (Object listValue : (List) value) { + else if (value instanceof List values) { + List writableList = new ArrayList<>(values.size()); + for (Object listValue : values) { writableList.add(convertHeaderValueIfNecessary(listValue)); } value = writableList; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/RabbitExceptionTranslator.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/RabbitExceptionTranslator.java index d553473dc0..ce1e460eca 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/RabbitExceptionTranslator.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/RabbitExceptionTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -51,8 +51,8 @@ private RabbitExceptionTranslator() { public static RuntimeException convertRabbitAccessException(Throwable ex) { Assert.notNull(ex, "Exception must not be null"); - if (ex instanceof AmqpException) { - return (AmqpException) ex; + if (ex instanceof AmqpException amqpException) { + return amqpException; } if (ex instanceof ShutdownSignalException sigEx) { return new AmqpConnectException(sigEx); @@ -75,8 +75,8 @@ public static RuntimeException convertRabbitAccessException(Throwable ex) { if (ex instanceof ConsumerCancelledException) { return new org.springframework.amqp.rabbit.support.ConsumerCancelledException(ex); } - if (ex instanceof org.springframework.amqp.rabbit.support.ConsumerCancelledException) { - return (org.springframework.amqp.rabbit.support.ConsumerCancelledException) ex; + if (ex instanceof org.springframework.amqp.rabbit.support.ConsumerCancelledException consumerCancelledException) { + return consumerCancelledException; } // fallback return new UncategorizedAmqpException(ex); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java index c59921a711..5c91161892 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java @@ -1806,12 +1806,12 @@ public boolean supports(Class clazz) { @Override public void validate(Object target, Errors errors) { - if (target instanceof ValidatedClass) { - if (((ValidatedClass) target).getBar() > 10) { + if (target instanceof ValidatedClass validatedClass) { + if (validatedClass.getBar() > 10) { errors.reject("bar too large"); } else { - ((ValidatedClass) target).setValidated(true); + validatedClass.setValidated(true); } } } @@ -2095,8 +2095,8 @@ public String qux(@Header("amqp_receivedRoutingKey") String rk, @NonNull @Payloa @RabbitHandler(isDefault = true) public String defaultHandler(@Payload Object payload) { - if (payload instanceof Foo) { - return "FOO: " + ((Foo) payload).field + " handled by default handler"; + if (payload instanceof Foo foo) { + return "FOO: " + foo.field + " handled by default handler"; } return payload.toString() + " handled by default handler"; } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerFactoryIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerFactoryIntegrationTests.java index efa50d529e..87fc06907b 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerFactoryIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerFactoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -77,8 +77,8 @@ private void invokeListener(RabbitListenerEndpoint endpoint, Message message) th SimpleMessageListenerContainer messageListenerContainer = containerFactory.createListenerContainer(endpoint); Object listener = messageListenerContainer.getMessageListener(); - if (listener instanceof ChannelAwareMessageListener) { - ((ChannelAwareMessageListener) listener).onMessage(message, mock(Channel.class)); + if (listener instanceof ChannelAwareMessageListener awareMessageListener) { + awareMessageListener.onMessage(message, mock(Channel.class)); } else { ((MessageListener) listener).onMessage(message); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumerIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumerIntegrationTests.java index c394e6bfe6..87cc18c71f 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumerIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -77,8 +77,8 @@ public void testTransactionalLowLevel() throws Exception { CountDownLatch latch = new CountDownLatch(2); List events = new ArrayList<>(); blockingQueueConsumer.setApplicationEventPublisher(e -> { - if (e instanceof ConsumeOkEvent) { - events.add((ConsumeOkEvent) e); + if (e instanceof ConsumeOkEvent consumeOkEvent) { + events.add(consumeOkEvent); latch.countDown(); } }); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerErrorHandlerIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerErrorHandlerIntegrationTests.java index cba98cee3f..1d28abe78b 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerErrorHandlerIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerErrorHandlerIntegrationTests.java @@ -257,8 +257,8 @@ private void testRejectingErrorHandler(RabbitTemplate template, AbstractMessageL // Verify that the exception strategy has access to the message final AtomicReference failed = new AtomicReference(); ConditionalRejectingErrorHandler eh = new ConditionalRejectingErrorHandler(t -> { - if (t instanceof ListenerExecutionFailedException) { - failed.set(((ListenerExecutionFailedException) t).getFailedMessage()); + if (t instanceof ListenerExecutionFailedException exception) { + failed.set(exception.getFailedMessage()); } return t instanceof ListenerExecutionFailedException && t.getCause() instanceof MessageConversionException; diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java index 4e3d4f6546..c9fe35fa46 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java @@ -220,8 +220,8 @@ public void publishEvent(Object event) { @Override public void publishEvent(ApplicationEvent event) { - if (event instanceof AsyncConsumerStartedEvent) { - newConsumer.set(((AsyncConsumerStartedEvent) event).getConsumer()); + if (event instanceof AsyncConsumerStartedEvent asyncConsumerStartedEvent) { + newConsumer.set(asyncConsumerStartedEvent.getConsumer()); latch2.countDown(); } } From bb73d05dbdf5d20ac22381a6de13f6b28f84cb63 Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Sat, 7 Sep 2024 02:58:43 +0700 Subject: [PATCH 534/737] GH-2809: Fix MultiRabbitListenerAnnotationBeanPostProcessor Fixes: #2809 Issue link: https://github.com/spring-projects/spring-amqp/issues/2809 * Add `BeanNameAware` to the `RabbitListenerContainerFactory` and `getBeanName()` * Use `getBeanName()` from `RabbitListenerContainerFactory` and `RabbitAdmin` in the `MultiRabbitListenerAnnotationBeanPostProcessor.resolveMultiRabbitAdminName()` --- ...itListenerAnnotationBeanPostProcessor.java | 35 +++++++++++++++---- ...itListenerAnnotationBeanPostProcessor.java | 5 +-- .../BaseRabbitListenerContainerFactory.java | 13 +++++++ .../RabbitListenerContainerFactory.java | 21 +++++++++-- .../AbstractRabbitAnnotationDrivenTests.java | 7 ++++ .../RabbitListenerContainerTestFactory.java | 13 ++++++- 6 files changed, 82 insertions(+), 12 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/MultiRabbitListenerAnnotationBeanPostProcessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/MultiRabbitListenerAnnotationBeanPostProcessor.java index 939bd6b583..0764691e1e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/MultiRabbitListenerAnnotationBeanPostProcessor.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/MultiRabbitListenerAnnotationBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-2024 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. @@ -24,6 +24,8 @@ import org.springframework.amqp.core.Declarable; import org.springframework.amqp.rabbit.config.RabbitListenerConfigUtils; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory; import org.springframework.util.StringUtils; /** @@ -35,6 +37,7 @@ * configuration, preventing the server from automatic binding non-related structures. * * @author Wander Costa + * @author Ngoc Nhan * * @since 2.3 */ @@ -70,14 +73,32 @@ private RabbitListener proxyIfAdminNotPresent(final RabbitListener rabbitListene * @return The name of the RabbitAdmin bean. */ protected String resolveMultiRabbitAdminName(RabbitListener rabbitListener) { - String admin = super.resolveExpressionAsString(rabbitListener.admin(), "admin"); - if (!StringUtils.hasText(admin) && StringUtils.hasText(rabbitListener.containerFactory())) { - admin = rabbitListener.containerFactory() + RabbitListenerConfigUtils.MULTI_RABBIT_ADMIN_SUFFIX; + + var admin = rabbitListener.admin(); + if (StringUtils.hasText(admin)) { + + var resolved = super.resolveExpression(admin); + if (resolved instanceof RabbitAdmin rabbitAdmin) { + + return rabbitAdmin.getBeanName(); + } + + return super.resolveExpressionAsString(admin, "admin"); } - if (!StringUtils.hasText(admin)) { - admin = RabbitListenerConfigUtils.RABBIT_ADMIN_BEAN_NAME; + + var containerFactory = rabbitListener.containerFactory(); + if (StringUtils.hasText(containerFactory)) { + + var resolved = super.resolveExpression(containerFactory); + if (resolved instanceof RabbitListenerContainerFactory rlcf) { + + return rlcf.getBeanName() + RabbitListenerConfigUtils.MULTI_RABBIT_ADMIN_SUFFIX; + } + + return containerFactory + RabbitListenerConfigUtils.MULTI_RABBIT_ADMIN_SUFFIX; } - return admin; + + return RabbitListenerConfigUtils.RABBIT_ADMIN_BEAN_NAME; } /** diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java index ffdd652657..6946bb28e8 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2024 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. @@ -119,6 +119,7 @@ * @author Gary Russell * @author Alex Panchenko * @author Artem Bilan + * @author Ngoc Nhan * * @since 1.4 * @@ -961,7 +962,7 @@ else if (resolved instanceof Integer) { } } - private Object resolveExpression(String value) { + protected Object resolveExpression(String value) { String resolvedValue = resolve(value); return this.resolver.evaluate(resolvedValue, this.expressionContext); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BaseRabbitListenerContainerFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BaseRabbitListenerContainerFactory.java index 5a1fab024a..6363bbe18c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BaseRabbitListenerContainerFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BaseRabbitListenerContainerFactory.java @@ -43,6 +43,7 @@ * @param the container type that the factory creates. * * @author Gary Russell + * @author Ngoc Nhan * @since 2.4 * */ @@ -67,6 +68,8 @@ public abstract class BaseRabbitListenerContainerFactory the container type. * @author Stephane Nicoll * @author Gary Russell + * @author Ngoc Nhan * @since 1.4 * @see RabbitListenerEndpoint */ @FunctionalInterface -public interface RabbitListenerContainerFactory { +public interface RabbitListenerContainerFactory extends BeanNameAware { /** * Create a {@link MessageListenerContainer} for the given @@ -48,4 +50,19 @@ default C createListenerContainer() { return createListenerContainer(null); } + @Override + default void setBeanName(String name) { + + } + + /** + * Return a bean name of the component or null if not a bean. + * @return the bean name. + * @since 3.2 + */ + @Nullable + default String getBeanName() { + return null; + } + } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AbstractRabbitAnnotationDrivenTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AbstractRabbitAnnotationDrivenTests.java index 271731d2af..bdfb6abbca 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AbstractRabbitAnnotationDrivenTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AbstractRabbitAnnotationDrivenTests.java @@ -48,6 +48,7 @@ * * @author Stephane Nicoll * @author Gary Russell + * @author Ngoc Nhan */ public abstract class AbstractRabbitAnnotationDrivenTests { @@ -87,7 +88,9 @@ public void testSampleConfiguration(ApplicationContext context, int expectedDefa context.getBean("rabbitListenerContainerFactory", RabbitListenerContainerTestFactory.class); RabbitListenerContainerTestFactory simpleFactory = context.getBean("simpleFactory", RabbitListenerContainerTestFactory.class); + assertThat(defaultFactory.getBeanName()).isEqualTo("rabbitListenerContainerFactory"); assertThat(defaultFactory.getListenerContainers()).hasSize(expectedDefaultContainers); + assertThat(simpleFactory.getBeanName()).isEqualTo("simpleFactory"); assertThat(simpleFactory.getListenerContainers()).hasSize(1); Map queues = context .getBeansOfType(org.springframework.amqp.core.Queue.class); @@ -129,6 +132,7 @@ private void checkAdmin(Collection admins) { public void testFullConfiguration(ApplicationContext context) { RabbitListenerContainerTestFactory simpleFactory = context.getBean("simpleFactory", RabbitListenerContainerTestFactory.class); + assertThat(simpleFactory.getBeanName()).isEqualTo("simpleFactory"); assertThat(simpleFactory.getListenerContainers()).hasSize(1); MethodRabbitListenerEndpoint endpoint = (MethodRabbitListenerEndpoint) simpleFactory.getListenerContainers().get(0).getEndpoint(); @@ -168,7 +172,9 @@ public void testCustomConfiguration(ApplicationContext context) { context.getBean("rabbitListenerContainerFactory", RabbitListenerContainerTestFactory.class); RabbitListenerContainerTestFactory customFactory = context.getBean("customFactory", RabbitListenerContainerTestFactory.class); + assertThat(defaultFactory.getBeanName()).isEqualTo("rabbitListenerContainerFactory"); assertThat(defaultFactory.getListenerContainers()).hasSize(1); + assertThat(customFactory.getBeanName()).isEqualTo("customFactory"); assertThat(customFactory.getListenerContainers()).hasSize(1); RabbitListenerEndpoint endpoint = defaultFactory.getListenerContainers().get(0).getEndpoint(); assertThat(endpoint.getClass()).as("Wrong endpoint type").isEqualTo(SimpleRabbitListenerEndpoint.class); @@ -191,6 +197,7 @@ public void testCustomConfiguration(ApplicationContext context) { public void testExplicitContainerFactoryConfiguration(ApplicationContext context) { RabbitListenerContainerTestFactory defaultFactory = context.getBean("simpleFactory", RabbitListenerContainerTestFactory.class); + assertThat(defaultFactory.getBeanName()).isEqualTo("simpleFactory"); assertThat(defaultFactory.getListenerContainers()).hasSize(1); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerTestFactory.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerTestFactory.java index fa592274fb..9e68785e27 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerTestFactory.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerTestFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2024 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. @@ -40,6 +40,8 @@ public class RabbitListenerContainerTestFactory implements RabbitListenerContain private final Map listenerContainers = new LinkedHashMap(); + private String beanName; + public List getListenerContainers() { return new ArrayList(this.listenerContainers.values()); } @@ -63,4 +65,13 @@ public MessageListenerTestContainer createListenerContainer(RabbitListenerEndpoi return container; } + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + public String getBeanName() { + return this.beanName; + } + } From 8e9c9e40363ad85d02d4ca84ae431b4a1e0adbae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 7 Sep 2024 02:57:20 +0000 Subject: [PATCH 535/737] Bump com.github.spotbugs in the development-dependencies group (#2812) Bumps the development-dependencies group with 1 update: com.github.spotbugs. Updates `com.github.spotbugs` from 6.0.21 to 6.0.22 --- updated-dependencies: - dependency-name: com.github.spotbugs dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b7eb6a7dc7..740da931ea 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ plugins { id 'io.spring.dependency-management' version '1.1.6' apply false id 'org.antora' version '1.0.0' id 'io.spring.antora.generate-antora-yml' version '0.0.1' - id 'com.github.spotbugs' version '6.0.21' + id 'com.github.spotbugs' version '6.0.22' id 'io.freefair.aggregate-javadoc' version '8.6' } From 104a6e1fad6860812e97be17eba83c4e6c164759 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 7 Sep 2024 02:59:16 +0000 Subject: [PATCH 536/737] Bump ch.qos.logback:logback-classic from 1.5.7 to 1.5.8 (#2813) Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.7 to 1.5.8. - [Commits](https://github.com/qos-ch/logback/compare/v_1.5.7...v_1.5.8) --- updated-dependencies: - dependency-name: ch.qos.logback:logback-classic dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 740da931ea..fdcb43990a 100644 --- a/build.gradle +++ b/build.gradle @@ -59,7 +59,7 @@ ext { junitJupiterVersion = '5.11.0' kotlinCoroutinesVersion = '1.8.1' log4jVersion = '2.23.1' - logbackVersion = '1.5.7' + logbackVersion = '1.5.8' lz4Version = '1.8.0' micrometerDocsVersion = '1.0.3' micrometerVersion = '1.14.0-SNAPSHOT' From c5bd5424e37dd8867b0c79530f59477966fa0077 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 14 Sep 2024 02:26:02 +0000 Subject: [PATCH 537/737] Bump the development-dependencies group with 2 updates (#2817) Bumps the development-dependencies group with 2 updates: [io.micrometer:micrometer-docs-generator](https://github.com/micrometer-metrics/micrometer-docs-generator) and [io.spring.develocity.conventions](https://github.com/spring-io/develocity-conventions). Updates `io.micrometer:micrometer-docs-generator` from 1.0.3 to 1.0.4 - [Release notes](https://github.com/micrometer-metrics/micrometer-docs-generator/releases) - [Commits](https://github.com/micrometer-metrics/micrometer-docs-generator/compare/v1.0.3...v1.0.4) Updates `io.spring.develocity.conventions` from 0.0.20 to 0.0.21 - [Release notes](https://github.com/spring-io/develocity-conventions/releases) - [Commits](https://github.com/spring-io/develocity-conventions/compare/v0.0.20...v0.0.21) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-docs-generator dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies - dependency-name: io.spring.develocity.conventions dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- settings.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index fdcb43990a..da05b55a2e 100644 --- a/build.gradle +++ b/build.gradle @@ -61,7 +61,7 @@ ext { log4jVersion = '2.23.1' logbackVersion = '1.5.8' lz4Version = '1.8.0' - micrometerDocsVersion = '1.0.3' + micrometerDocsVersion = '1.0.4' micrometerVersion = '1.14.0-SNAPSHOT' micrometerTracingVersion = '1.4.0-SNAPSHOT' mockitoVersion = '5.12.0' diff --git a/settings.gradle b/settings.gradle index 4e1e2619ab..5c20e31b03 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,7 +7,7 @@ pluginManagement { plugins { id 'com.gradle.develocity' version '3.17.6' - id 'io.spring.develocity.conventions' version '0.0.20' + id 'io.spring.develocity.conventions' version '0.0.21' } rootProject.name = 'spring-amqp-dist' From 762d6a6914d26559856935172adfbe3b7f2265e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 14 Sep 2024 02:26:10 +0000 Subject: [PATCH 538/737] Bump org.springframework.retry:spring-retry from 2.0.8 to 2.0.9 (#2820) Bumps [org.springframework.retry:spring-retry](https://github.com/spring-projects/spring-retry) from 2.0.8 to 2.0.9. - [Release notes](https://github.com/spring-projects/spring-retry/releases) - [Commits](https://github.com/spring-projects/spring-retry/compare/v2.0.8...v2.0.9) --- updated-dependencies: - dependency-name: org.springframework.retry:spring-retry dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index da05b55a2e..73cf6e25f1 100644 --- a/build.gradle +++ b/build.gradle @@ -70,7 +70,7 @@ ext { reactorVersion = '2024.0.0-SNAPSHOT' snappyVersion = '1.1.10.6' springDataVersion = '2024.0.3' - springRetryVersion = '2.0.8' + springRetryVersion = '2.0.9' springVersion = '6.2.0-SNAPSHOT' testcontainersVersion = '1.19.8' zstdJniVersion = '1.5.6-5' From d603111d79191aa50529c811fcb1c48c8ae2e017 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 14 Sep 2024 02:29:40 +0000 Subject: [PATCH 539/737] Bump org.springframework.data:spring-data-bom from 2024.0.3 to 2024.0.4 (#2819) Bumps [org.springframework.data:spring-data-bom](https://github.com/spring-projects/spring-data-bom) from 2024.0.3 to 2024.0.4. - [Release notes](https://github.com/spring-projects/spring-data-bom/releases) - [Commits](https://github.com/spring-projects/spring-data-bom/compare/2024.0.3...2024.0.4) --- updated-dependencies: - dependency-name: org.springframework.data:spring-data-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 73cf6e25f1..ca35fd8932 100644 --- a/build.gradle +++ b/build.gradle @@ -69,7 +69,7 @@ ext { rabbitmqVersion = '5.21.0' reactorVersion = '2024.0.0-SNAPSHOT' snappyVersion = '1.1.10.6' - springDataVersion = '2024.0.3' + springDataVersion = '2024.0.4' springRetryVersion = '2.0.9' springVersion = '6.2.0-SNAPSHOT' testcontainersVersion = '1.19.8' From 5db0ee250896d726730cfe1f5313b191a7335b30 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 14 Sep 2024 02:33:44 +0000 Subject: [PATCH 540/737] Bump org.xerial.snappy:snappy-java from 1.1.10.6 to 1.1.10.7 (#2818) Bumps [org.xerial.snappy:snappy-java](https://github.com/xerial/snappy-java) from 1.1.10.6 to 1.1.10.7. - [Release notes](https://github.com/xerial/snappy-java/releases) - [Commits](https://github.com/xerial/snappy-java/compare/v1.1.10.6...v1.1.10.7) --- updated-dependencies: - dependency-name: org.xerial.snappy:snappy-java dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ca35fd8932..fddd060b39 100644 --- a/build.gradle +++ b/build.gradle @@ -68,7 +68,7 @@ ext { rabbitmqStreamVersion = '0.15.0' rabbitmqVersion = '5.21.0' reactorVersion = '2024.0.0-SNAPSHOT' - snappyVersion = '1.1.10.6' + snappyVersion = '1.1.10.7' springDataVersion = '2024.0.4' springRetryVersion = '2.0.9' springVersion = '6.2.0-SNAPSHOT' From 7d1e696d7f80d934999a11136e7812acfecee9db Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Mon, 16 Sep 2024 22:09:40 +0700 Subject: [PATCH 541/737] Exchange/routingKey as independet props in the RabbitMessageSenderContext --- .../amqp/rabbit/core/RabbitTemplate.java | 3 +- .../RabbitMessageSenderContext.java | 47 ++++++++++++++++++- .../support/micrometer/ObservationTests.java | 13 +++-- 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java index dc33e807d8..5d2894c96c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java @@ -159,6 +159,7 @@ * @author Mohammad Hewedy * @author Alexey Platonov * @author Leonardo Ferreira + * @author Ngoc Nhan * * @since 1.0 */ @@ -2486,7 +2487,7 @@ protected void observeTheSend(Channel channel, Message message, boolean mandator ObservationRegistry registry = getObservationRegistry(); Observation observation = RabbitTemplateObservation.TEMPLATE_OBSERVATION.observation(this.observationConvention, DefaultRabbitTemplateObservationConvention.INSTANCE, - () -> new RabbitMessageSenderContext(message, this.beanName, exch + "/" + rKey), registry); + () -> new RabbitMessageSenderContext(message, this.beanName, exch, rKey), registry); observation.observe(() -> sendToRabbit(channel, exch, rKey, mandatory, message)); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageSenderContext.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageSenderContext.java index e327f6ebc6..b7994ad645 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageSenderContext.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageSenderContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2024 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. @@ -24,6 +24,7 @@ * {@link SenderContext} for {@link Message}s. * * @author Gary Russell + * @author Ngoc Nhan * @since 3.0 * */ @@ -33,14 +34,40 @@ public class RabbitMessageSenderContext extends SenderContext { private final String destination; + private final String exchange; + + private final String routingKey; + + @Deprecated(since = "3.2") public RabbitMessageSenderContext(Message message, String beanName, String destination) { super((carrier, key, value) -> message.getMessageProperties().setHeader(key, value)); setCarrier(message); this.beanName = beanName; + this.exchange = null; + this.routingKey = null; this.destination = destination; setRemoteServiceName("RabbitMQ"); } + + /** + * Create an instance {@code RabbitMessageSenderContext}. + * @param message a message to send + * @param beanName the bean name + * @param exchange the name of the exchange + * @param routingKey the routing key + * @since 3.2 + */ + public RabbitMessageSenderContext(Message message, String beanName, String exchange, String routingKey) { + super((carrier, key, value) -> message.getMessageProperties().setHeader(key, value)); + setCarrier(message); + this.beanName = beanName; + this.exchange = exchange; + this.routingKey = routingKey; + this.destination = exchange + "/" + routingKey; + setRemoteServiceName("RabbitMQ"); + } + public String getBeanName() { return this.beanName; } @@ -53,4 +80,22 @@ public String getDestination() { return this.destination; } + /** + * Return the exchange. + * @return the exchange. + * @since 3.2 + */ + public String getExchange() { + return this.exchange; + } + + /** + * Return the routingKey. + * @return the routingKey. + * @since 3.2 + */ + public String getRoutingKey() { + return this.routingKey; + } + } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java index e1787df31f..4c0b6fa056 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2024 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. @@ -66,6 +66,7 @@ /** * @author Gary Russell + * @author Ngoc Nhan * @since 3.0 * */ @@ -110,7 +111,9 @@ void endToEnd(@Autowired Listener listener, @Autowired RabbitTemplate template, @Override public KeyValues getLowCardinalityKeyValues(RabbitMessageSenderContext context) { - return super.getLowCardinalityKeyValues(context).and("foo", "bar"); + return super.getLowCardinalityKeyValues(context).and("foo", "bar") + .and("messaging.destination.name", context.getExchange()) + .and("messaging.rabbitmq.destination.routing_key", context.getRoutingKey()); } }); @@ -135,6 +138,8 @@ public KeyValues getLowCardinalityKeyValues(RabbitMessageReceiverContext context span = spans.poll(); assertThat(span.getTags()).containsEntry("spring.rabbit.template.name", "template"); assertThat(span.getTags()).containsEntry("foo", "bar"); + assertThat(span.getTags()).containsEntry("messaging.destination.name", ""); + assertThat(span.getTags()).containsEntry("messaging.rabbitmq.destination.routing_key", "observation.testQ1"); assertThat(span.getName()).isEqualTo("/observation.testQ1 send"); await().until(() -> spans.peekFirst().getTags().size() == 4); span = spans.poll(); @@ -142,10 +147,12 @@ public KeyValues getLowCardinalityKeyValues(RabbitMessageReceiverContext context .containsAllEntriesOf(Map.of("spring.rabbit.listener.id", "obs1", "foo", "some foo value", "bar", "some bar value", "baz", "qux")); assertThat(span.getName()).isEqualTo("observation.testQ1 receive"); - await().until(() -> spans.peekFirst().getTags().size() == 2); + await().until(() -> spans.peekFirst().getTags().size() == 4); span = spans.poll(); assertThat(span.getTags()).containsEntry("spring.rabbit.template.name", "template"); assertThat(span.getTags()).containsEntry("foo", "bar"); + assertThat(span.getTags()).containsEntry("messaging.destination.name", ""); + assertThat(span.getTags()).containsEntry("messaging.rabbitmq.destination.routing_key", "observation.testQ2"); assertThat(span.getName()).isEqualTo("/observation.testQ2 send"); await().until(() -> spans.peekFirst().getTags().size() == 3); span = spans.poll(); From 2ebc7ef1234d5a6eced160bb132f392fbe84f92a Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Mon, 16 Sep 2024 23:39:12 +0700 Subject: [PATCH 542/737] GH-2815: Fix `RabbitAdmin` for static `Declarables Fixes: #2815 Issue link: https://github.com/spring-projects/spring-amqp/issues/2815 When rabbitmq resets, `RabbitAdmin#initialize` calls `redeclareManualDeclarables()` first, before it continues with re-declaring statically configured declarations. This means that explicitly declared Bindings to statically declared Exchanges can never be restored, because the Exchange has to exist in order for the Binding declaration to succeed. * Call new `redeclareBeanDeclarables()` before `redeclareManualDeclarables()` **Auto-cherry-pick to `3.1.x`** --- .../org/springframework/amqp/rabbit/core/RabbitAdmin.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java index b86dc8063a..b85df8fece 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java @@ -81,6 +81,7 @@ * @author Gary Russell * @author Artem Bilan * @author Christian Tzolov + * @author Ngoc Nhan */ @ManagedResource(description = "Admin Tasks") public class RabbitAdmin implements AmqpAdmin, ApplicationContextAware, ApplicationEventPublisherAware, @@ -648,8 +649,14 @@ public void afterPropertiesSet() { @Override // NOSONAR complexity public void initialize() { + redeclareBeanDeclarables(); redeclareManualDeclarables(); + } + /** + * Process bean declarables. + */ + private void redeclareBeanDeclarables() { if (this.applicationContext == null) { this.logger.debug("no ApplicationContext has been set, cannot auto-declare Exchanges, Queues, and Bindings"); return; From c6dcc76ae9bbf35caaec5d2d6cefa19232e8be49 Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Mon, 16 Sep 2024 23:56:13 +0700 Subject: [PATCH 543/737] Fix links in docs * Fix links in section Previous Releases * Declare some attributes represent document links --- src/reference/antora/antora.yml | 13 ++++++++++++- .../ROOT/pages/amqp/broker-configuration.adoc | 2 +- .../antora/modules/ROOT/pages/amqp/connections.adoc | 2 +- .../modules/ROOT/pages/amqp/listener-queues.adoc | 2 +- .../ROOT/pages/amqp/management-rest-api.adoc | 2 +- .../modules/ROOT/pages/amqp/message-converters.adoc | 2 +- .../async-annotation-driven/enable.adoc | 2 +- .../receiving-messages/micrometer-observation.adoc | 2 +- .../pages/amqp/receiving-messages/micrometer.adoc | 2 +- .../modules/ROOT/pages/amqp/request-reply.adoc | 4 ++-- ...-recovering-from-errors-and-broker-failures.adoc | 2 +- .../modules/ROOT/pages/amqp/sending-messages.adoc | 2 +- .../modules/ROOT/pages/amqp/transactions.adoc | 8 ++++---- .../antora/modules/ROOT/pages/appendix/native.adoc | 2 +- .../changes-in-1-3-since-1-2.adoc | 13 ++++++------- .../changes-in-1-4-since-1-3.adoc | 8 ++++---- .../changes-in-1-5-since-1-4.adoc | 6 +++--- .../changes-in-1-6-since-1-5.adoc | 4 ++-- .../changes-in-1-7-since-1-6.adoc | 2 +- .../changes-in-2-0-since-1-7.adoc | 4 ++-- .../changes-in-2-1-since-2-0.adoc | 2 +- .../changes-to-1-2-since-1-1.adoc | 2 +- .../modules/ROOT/pages/integration-reference.adoc | 2 +- .../antora/modules/ROOT/pages/logging.adoc | 2 +- .../antora/modules/ROOT/pages/sample-apps.adoc | 6 +++--- src/reference/antora/modules/ROOT/pages/stream.adoc | 8 ++++---- 26 files changed, 58 insertions(+), 48 deletions(-) diff --git a/src/reference/antora/antora.yml b/src/reference/antora/antora.yml index fc3041c337..f3ceb248f4 100644 --- a/src/reference/antora/antora.yml +++ b/src/reference/antora/antora.yml @@ -14,4 +14,15 @@ ext: asciidoc: attributes: attribute-missing: 'warn' - chomp: 'all' \ No newline at end of file + chomp: 'all' + spring-docs: 'https://docs.spring.io' + spring-framework-docs: '{spring-docs}/spring-framework/reference' + spring-integration-docs: '{spring-docs}/spring-integration/reference' + spring-amqp-java-docs: '{spring-docs}/spring-amqp/docs/current/api/org/springframework/amqp' + spring-framework-java-docs: '{spring-docs}/spring/docs/current/javadoc-api/org/springframework' + spring-retry-java-docs: '{spring-docs}/spring-retry/docs/api/current/' + # External projects URLs and related attributes + micrometer-docs: 'https://docs.micrometer.io' + micrometer-tracing-docs: '{micrometer-docs}/tracing/reference/' + micrometer-micrometer-docs: '{micrometer-docs}/micrometer/reference/' + rabbitmq-stream-docs: 'https://rabbitmq.github.io/rabbitmq-stream-java-client/stable/htmlsingle' \ No newline at end of file diff --git a/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc b/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc index c983716a37..1dbf6ac314 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc @@ -359,7 +359,7 @@ public Exchange exchange() { } ---- -See the Javadoc for https://docs.spring.io/spring-amqp/docs/current/api/org/springframework/amqp/core/QueueBuilder.html[`org.springframework.amqp.core.QueueBuilder`] and https://docs.spring.io/spring-amqp/docs/current/api/org/springframework/amqp/core/ExchangeBuilder.html[`org.springframework.amqp.core.ExchangeBuilder`] for more information. +See the Javadoc for {spring-amqp-java-docs}/core/QueueBuilder.html[`org.springframework.amqp.core.QueueBuilder`] and {spring-amqp-java-docs}/core/ExchangeBuilder.html[`org.springframework.amqp.core.ExchangeBuilder`] for more information. Starting with version 2.0, the `ExchangeBuilder` now creates durable exchanges by default, to be consistent with the simple constructors on the individual `AbstractExchange` classes. To make a non-durable exchange with the builder, use `.durable(false)` before invoking `.build()`. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc b/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc index 42a4b0a199..da8bf568b8 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc @@ -483,7 +483,7 @@ public class MyService { ---- It is important to unbind the resource after use. -For more information, see the https://docs.spring.io/spring-amqp/docs/current/api/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.html[JavaDoc] for `AbstractRoutingConnectionFactory`. +For more information, see the {spring-amqp-java-docs}/rabbit/connection/AbstractRoutingConnectionFactory.html[JavaDoc] for `AbstractRoutingConnectionFactory`. Starting with version 1.4, `RabbitTemplate` supports the SpEL `sendConnectionFactorySelectorExpression` and `receiveConnectionFactorySelectorExpression` properties, which are evaluated on each AMQP protocol interaction operation (`send`, `sendAndReceive`, `receive`, or `receiveAndReply`), resolving to a `lookupKey` value for the provided `AbstractRoutingConnectionFactory`. You can use bean references, such as `@vHostResolver.getVHost(#root)` in the expression. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/listener-queues.adoc b/src/reference/antora/modules/ROOT/pages/amqp/listener-queues.adoc index 03fe292c7a..ed6101ec5c 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/listener-queues.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/listener-queues.adoc @@ -8,7 +8,7 @@ Container can be initially configured to listen on zero queues. Queues can be added and removed at runtime. The `SimpleMessageListenerContainer` recycles (cancels and re-creates) all consumers when any pre-fetched messages have been processed. The `DirectMessageListenerContainer` creates/cancels individual consumer(s) for each queue without affecting consumers on other queues. -See the https://docs.spring.io/spring-amqp/docs/current/api/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.html[Javadoc] for the `addQueues`, `addQueueNames`, `removeQueues` and `removeQueueNames` methods. +See the {spring-amqp-java-docs}/rabbit/listener/AbstractMessageListenerContainer.html[Javadoc] for the `addQueues`, `addQueueNames`, `removeQueues` and `removeQueueNames` methods. If not all queues are available, the container tries to passively declare (and consume from) the missing queues every 60 seconds. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/management-rest-api.adoc b/src/reference/antora/modules/ROOT/pages/amqp/management-rest-api.adoc index 445b46c8a1..a693e1e36d 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/management-rest-api.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/management-rest-api.adoc @@ -5,7 +5,7 @@ When the management plugin is enabled, the RabbitMQ server exposes a REST API to monitor and configure the broker. A https://github.com/rabbitmq/hop[Java Binding for the API] is now provided. The `com.rabbitmq.http.client.Client` is a standard, immediate, and, therefore, blocking API. -It is based on the https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#spring-web[Spring Web] module and its `RestTemplate` implementation. +It is based on the {spring-framework-docs}/web.html[Spring Web] module and its `RestTemplate` implementation. On the other hand, the `com.rabbitmq.http.client.ReactorNettyClient` is a reactive, non-blocking implementation based on the https://projectreactor.io/docs/netty/release/reference/docs/index.html[Reactor Netty] project. The hop dependency (`com.rabbitmq:http-client`) is now also `optional`. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/message-converters.adoc b/src/reference/antora/modules/ROOT/pages/amqp/message-converters.adoc index b828342c4b..c028af61bd 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/message-converters.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/message-converters.adoc @@ -356,7 +356,7 @@ It has been replaced by `AbstractJackson2MessageConverter`. Yet another option is the `MarshallingMessageConverter`. It delegates to the Spring OXM library's implementations of the `Marshaller` and `Unmarshaller` strategy interfaces. -You can read more about that library https://docs.spring.io/spring/docs/current/spring-framework-reference/html/oxm.html[here]. +You can read more about that library {spring-framework-docs}/data-access/oxm.html[here]. In terms of configuration, it is most common to provide only the constructor argument, since most implementations of `Marshaller` also implement `Unmarshaller`. The following example shows how to configure a `MarshallingMessageConverter`: diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/enable.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/enable.adoc index 71d39dc08b..422de556ef 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/enable.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/enable.adoc @@ -37,7 +37,7 @@ In this case, and ignoring the RabbitMQ infrastructure setup, the `processOrder` You can customize the listener container factory to use for each annotation, or you can configure an explicit default by implementing the `RabbitListenerConfigurer` interface. The default is required only if at least one endpoint is registered without a specific container factory. -See the https://docs.spring.io/spring-amqp/docs/current/api/org/springframework/amqp/rabbit/annotation/RabbitListenerConfigurer.html[Javadoc] for full details and examples. +See the {spring-amqp-java-docs}/rabbit/annotation/RabbitListenerConfigurer.html[Javadoc] for full details and examples. The container factories provide methods for adding `MessagePostProcessor` instances that are applied after receiving messages (before invoking the listener) and before sending replies. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer-observation.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer-observation.adoc index 7d30d09065..bbd1c340ee 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer-observation.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer-observation.adoc @@ -7,7 +7,7 @@ Using Micrometer for observation is now supported, since version 3.0, for the `R Set `observationEnabled` on each component to enable observation; this will disable xref:amqp/receiving-messages/micrometer.adoc[Micrometer Timers] because the timers will now be managed with each observation. When using annotated listeners, set `observationEnabled` on the container factory. -Refer to https://docs.micrometer.io/tracing/reference/[Micrometer Tracing] for more information. +Refer to {micrometer-tracing-docs}[Micrometer Tracing] for more information. To add tags to timers/traces, configure a custom `RabbitTemplateObservationConvention` or `RabbitListenerObservationConvention` to the template or listener container, respectively. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer.adoc index efae24ebfb..bc581083ad 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer.adoc @@ -2,7 +2,7 @@ = Micrometer Integration :page-section-summary-toc: 1 -NOTE: This section documents the integration with https://docs.micrometer.io/micrometer/reference/[Micrometer]. +NOTE: This section documents the integration with {micrometer-micrometer-docs}[Micrometer]. For integration with Micrometer Observation, see xref:amqp/receiving-messages/micrometer-observation.adoc[Micrometer Observation]. Starting with version 2.2, the listener containers will automatically create and update Micrometer `Timer` s for the listener, if `Micrometer` is detected on the class path, and a single `MeterRegistry` is present in the application context (or exactly one is annotated `@Primary`, such as when using Spring Boot). diff --git a/src/reference/antora/modules/ROOT/pages/amqp/request-reply.adoc b/src/reference/antora/modules/ROOT/pages/amqp/request-reply.adoc index 57d515d987..2a17bb8350 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/request-reply.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/request-reply.adoc @@ -6,11 +6,11 @@ Those methods are quite useful for request-reply scenarios, since they handle th Similar request-reply methods are also available where the `MessageConverter` is applied to both the request and reply. Those methods are named `convertSendAndReceive`. -See the https://docs.spring.io/spring-amqp/docs/current/api/org/springframework/amqp/core/AmqpTemplate.html[Javadoc of `AmqpTemplate`] for more detail. +See the {spring-amqp-java-docs}/core/AmqpTemplate.html[Javadoc of `AmqpTemplate`] for more detail. Starting with version 1.5.0, each of the `sendAndReceive` method variants has an overloaded version that takes `CorrelationData`. Together with a properly configured connection factory, this enables the receipt of publisher confirms for the send side of the operation. -See xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns] and the https://docs.spring.io/spring-amqp/docs/current/api/org/springframework/amqp/rabbit/core/RabbitOperations.html[Javadoc for `RabbitOperations`] for more information. +See xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns] and the {spring-amqp-java-docs}/rabbit/core/RabbitOperations.html[Javadoc for `RabbitOperations`] for more information. Starting with version 2.0, there are variants of these methods (`convertSendAndReceiveAsType`) that take an additional `ParameterizedTypeReference` argument to convert complex returned types. The template must be configured with a `SmartMessageConverter`. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/resilience-recovering-from-errors-and-broker-failures.adoc b/src/reference/antora/modules/ROOT/pages/amqp/resilience-recovering-from-errors-and-broker-failures.adoc index 18f5107668..f9c89b3e77 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/resilience-recovering-from-errors-and-broker-failures.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/resilience-recovering-from-errors-and-broker-failures.adoc @@ -90,7 +90,7 @@ public StatefulRetryOperationsInterceptor interceptor() { Only a subset of retry capabilities can be configured this way. More advanced features would need the configuration of a `RetryTemplate` as a Spring bean. -See the https://docs.spring.io/spring-retry/docs/api/current/[Spring Retry Javadoc] for complete information about available policies and their configuration. +See the {spring-retry-java-docs}[Spring Retry Javadoc] for complete information about available policies and their configuration. [[batch-retry]] == Retry with Batch Listeners diff --git a/src/reference/antora/modules/ROOT/pages/amqp/sending-messages.adoc b/src/reference/antora/modules/ROOT/pages/amqp/sending-messages.adoc index 961b8daa13..0d1eaa72da 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/sending-messages.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/sending-messages.adoc @@ -100,7 +100,7 @@ Message message = MessageBuilder.withBody("foo".getBytes()) .build(); ---- -Each of the properties defined on the https://docs.spring.io/spring-amqp/docs/current/api/org/springframework/amqp/core/MessageProperties.html[`MessageProperties`] can be set. +Each of the properties defined on the {spring-amqp-java-docs}/core/MessageProperties.html[`MessageProperties`] can be set. Other methods include `setHeader(String key, String value)`, `removeHeader(String key)`, `removeHeaders()`, and `copyProperties(MessageProperties properties)`. Each property setting method has a `set*IfAbsent()` variant. In the cases where a default initial value exists, the method is named `set*IfAbsentOrDefault()`. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/transactions.adoc b/src/reference/antora/modules/ROOT/pages/amqp/transactions.adoc index c007b02199..59bb842f7b 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/transactions.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/transactions.adoc @@ -69,7 +69,7 @@ If the `channelTransacted` flag was set to `false` (the default) in the precedin Prior to version 1.6.6, adding a rollback rule to a container's `transactionAttribute` when using an external transaction manager (such as JDBC) had no effect. Exceptions always rolled back the transaction. -Also, when using a https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/html/transaction.html#transaction-declarative[transaction advice] in the container's advice chain, conditional rollback was not very useful, because all listener exceptions are wrapped in a `ListenerExecutionFailedException`. +Also, when using a {spring-framework-docs}/data-access/transaction/declarative.html[transaction advice] in the container's advice chain, conditional rollback was not very useful, because all listener exceptions are wrapped in a `ListenerExecutionFailedException`. The first problem has been corrected, and the rules are now applied properly. Further, the `ListenerFailedRuleBasedTransactionAttribute` is now provided. @@ -116,13 +116,13 @@ See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration] [[using-rabbittransactionmanager]] == Using `RabbitTransactionManager` -The https://docs.spring.io/spring-amqp/docs/current/api/org/springframework/amqp/rabbit/transaction/RabbitTransactionManager.html[RabbitTransactionManager] is an alternative to executing Rabbit operations within, and synchronized with, external transactions. -This transaction manager is an implementation of the https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/transaction/PlatformTransactionManager.html[`PlatformTransactionManager`] interface and should be used with a single Rabbit `ConnectionFactory`. +The {spring-amqp-java-docs}/rabbit/transaction/RabbitTransactionManager.html[RabbitTransactionManager] is an alternative to executing Rabbit operations within, and synchronized with, external transactions. +This transaction manager is an implementation of the {spring-framework-java-docs}/transaction/PlatformTransactionManager.html[`PlatformTransactionManager`] interface and should be used with a single Rabbit `ConnectionFactory`. IMPORTANT: This strategy is not able to provide XA transactions -- for example, in order to share transactions between messaging and database access. Application code is required to retrieve the transactional Rabbit resources through `ConnectionFactoryUtils.getTransactionalResourceHolder(ConnectionFactory, boolean)` instead of a standard `Connection.createChannel()` call with subsequent channel creation. -When using Spring AMQP's https://docs.spring.io/spring-amqp/docs/latest_ga/api/org/springframework/amqp/rabbit/core/RabbitTemplate.html[RabbitTemplate], it will autodetect a thread-bound Channel and automatically participate in its transaction. +When using Spring AMQP's {spring-amqp-java-docs}/rabbit/core/RabbitTemplate.html[RabbitTemplate], it will autodetect a thread-bound Channel and automatically participate in its transaction. With Java Configuration, you can setup a new RabbitTransactionManager by using the following bean: diff --git a/src/reference/antora/modules/ROOT/pages/appendix/native.adoc b/src/reference/antora/modules/ROOT/pages/appendix/native.adoc index ee924671a2..8234dc91e6 100644 --- a/src/reference/antora/modules/ROOT/pages/appendix/native.adoc +++ b/src/reference/antora/modules/ROOT/pages/appendix/native.adoc @@ -2,6 +2,6 @@ = Native Images :page-section-summary-toc: 1 -https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aot[Spring AOT] native hints are provided to assist in developing native images for Spring applications that use Spring AMQP. +{spring-framework-docs}/core/aot.html[Spring AOT] native hints are provided to assist in developing native images for Spring applications that use Spring AMQP. Some examples can be seen in the https://github.com/spring-projects/spring-aot-smoke-tests/tree/main/integration[`spring-aot-smoke-tests` GitHub repository]. diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-3-since-1-2.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-3-since-1-2.adoc index da13067fff..7aa1b869ff 100644 --- a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-3-since-1-2.adoc +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-3-since-1-2.adoc @@ -5,14 +5,14 @@ == Listener Concurrency The listener container now supports dynamic scaling of the number of consumers based on workload, or you can programmatically change the concurrency without stopping the container. -See <>. +See xref:amqp/listener-concurrency.adoc#listener-concurrency[Listener Concurrency]. [[listener-queues]] == Listener Queues The listener container now permits the queues on which it listens to be modified at runtime. Also, the container now starts if at least one of its configured queues is available for use. -See <> +See xref:amqp/listener-queues.adoc#listener-queues[Listener Container Queues] This listener container now redeclares any auto-delete queues during startup. See xref:amqp/receiving-messages/async-consumer.adoc#lc-auto-delete[`auto-delete` Queues]. @@ -21,13 +21,13 @@ See xref:amqp/receiving-messages/async-consumer.adoc#lc-auto-delete[`auto-delete == Consumer Priority The listener container now supports consumer arguments, letting the `x-priority` argument be set. -See <>. +See xref:amqp/receiving-messages/async-consumer.adoc#consumer-priority[Consumer Priority]. [[exclusive-consumer]] == Exclusive Consumer You can now configure `SimpleMessageListenerContainer` with a single `exclusive` consumer, preventing other consumers from listening to the queue. -See <>. +See xref:amqp/exclusive-consumer.adoc[Exclusive Consumer]. [[rabbit-admin]] == Rabbit Admin @@ -47,7 +47,7 @@ If you wish to bind with an empty string routing key, you need to specify `key=" The `AmqpTemplate` now provides several synchronous `receiveAndReply` methods. These are implemented by the `RabbitTemplate`. -For more information see <>. +For more information see xref:amqp/receiving-messages.adoct[Receiving Messages]. The `RabbitTemplate` now supports configuring a `RetryTemplate` to attempt retries (with optional back-off policy) for when the broker is not available. For more information see xref:amqp/template.adoc#template-retry[Adding Retry Capabilities]. @@ -66,12 +66,11 @@ You can now configure the `` of the `` with a `key/va These options are mutually exclusive. See xref:amqp/broker-configuration.adoc#headers-exchange[Headers Exchange]. -[[routing-connection-factory]] == Routing Connection Factory A new `SimpleRoutingConnectionFactory` has been introduced. It allows configuration of `ConnectionFactories` mapping, to determine the target `ConnectionFactory` to use at runtime. -See <>. +See xref:amqp/connections.adoc#routing-connection-factory[routing-connection-factory]. [[messagebuilder-and-messagepropertiesbuilder]] == `MessageBuilder` and `MessagePropertiesBuilder` diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-4-since-1-3.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-4-since-1-3.adoc index a4346e4ba4..c43098a182 100644 --- a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-4-since-1-3.adoc +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-4-since-1-3.adoc @@ -35,14 +35,14 @@ See xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and `RabbitConnectionFactoryBean` creates the underlying RabbitMQ `ConnectionFactory` used by the `CachingConnectionFactory`. This enables configuration of SSL options using Spring's dependency injection. -See <>. +See xref:amqp/connections.adoc#connection-factory[Configuring the Underlying Client Connection Factory]. [[using-cachingconnectionfactory]] == Using `CachingConnectionFactory` The `CachingConnectionFactory` now lets the `connectionTimeout` be set as a property or as an attribute in the namespace. It sets the property on the underlying RabbitMQ `ConnectionFactory`. -See <>. +See xref:amqp/connections.adoc#connection-factory[Configuring the Underlying Client Connection Factory]. [[log-appender]] == Log Appender @@ -71,13 +71,13 @@ The `mandatoryExpression`, `sendConnectionFactorySelectorExpression`, and `recei The `mandatoryExpression` is used to evaluate a `mandatory` boolean value against each request message when a `ReturnCallback` is in use. See xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns]. The `sendConnectionFactorySelectorExpression` and `receiveConnectionFactorySelectorExpression` are used when an `AbstractRoutingConnectionFactory` is provided, to determine the `lookupKey` for the target `ConnectionFactory` at runtime on each AMQP protocol interaction operation. -See <>. +See xref:amqp/connections.adoc#routing-connection-factory[routing-connection-factory]. [[listeners-and-the-routing-connection-factory]] == Listeners and the Routing Connection Factory You can configure a `SimpleMessageListenerContainer` with a routing connection factory to enable connection selection based on the queue names. -See <>. +See xref:amqp/connections.adoc#routing-connection-factory[routing-connection-factory]. [[rabbittemplate:-recoverycallback-option]] == `RabbitTemplate`: `RecoveryCallback` Option diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-5-since-1-4.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-5-since-1-4.adoc index 29282a0033..4b5621cb0e 100644 --- a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-5-since-1-4.adoc +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-5-since-1-4.adoc @@ -5,7 +5,7 @@ == `spring-erlang` Is No Longer Supported The `spring-erlang` jar is no longer included in the distribution. -Use <> instead. +Use xref:amqp/management-rest-api.adoc#management-rest-api[RabbitMQ REST API] instead. [[cachingconnectionfactory-changes]] == `CachingConnectionFactory` Changes @@ -122,7 +122,7 @@ See xref:amqp/request-reply.adoc#reply-listener[Reply Listener Container] for mo == `RabbitManagementTemplate` Added The `RabbitManagementTemplate` has been introduced to monitor and configure the RabbitMQ Broker by using the REST API provided by its https://www.rabbitmq.com/management.html[management plugin]. -See <> for more information. +See xref:amqp/management-rest-api.adoc#management-rest-api[RabbitMQ REST API] for more information. [[listener-container-bean-names-xml]] == Listener Container Bean Names (XML) @@ -160,7 +160,7 @@ See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration] == Channel Close Logging A mechanism to control the log levels of channel closure has been introduced. -See <>. +See xref:amqp/connections.adoc#channel-close-logging[Logging Channel Close Events]. [[application-events]] == Application Events diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-6-since-1-5.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-6-since-1-5.adoc index 02d8037f46..2e96f8fea2 100644 --- a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-6-since-1-5.adoc +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-6-since-1-5.adoc @@ -189,7 +189,7 @@ See xref:amqp/receiving-messages/async-annotation-driven.adoc[Annotation-driven == Delayed Message Exchange Spring AMQP now has first class support for the RabbitMQ Delayed Message Exchange plugin. -See <> for more information. +See xref:amqp/delayed-message-exchange.adoc[Delayed Message Exchange] for more information. [[exchange-internal-flag]] == Exchange Internal Flag @@ -235,7 +235,7 @@ factory. You can now configure a "`allowed list`" of allowable classes when you use Java deserialization. You should consider creating an allowed list if you accept messages with serialized java objects from untrusted sources. -See <> for more information. +See amqp/message-converters.adoc#java-deserialization[Java Deserialization] for more information. [[json-messageconverter]] == JSON `MessageConverter` diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-7-since-1-6.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-7-since-1-6.adoc index 2c565b3025..054dd56e42 100644 --- a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-7-since-1-6.adoc +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-7-since-1-6.adoc @@ -51,7 +51,7 @@ The framework is no longer compatible with previous versions. == JUnit `@Rules` Rules that have previously been used internally by the framework have now been made available in a separate jar called `spring-rabbit-junit`. -See <> for more information. +See xref:testing.adoc#junit-rules[JUnit4 `@Rules`] for more information. [[container-conditional-rollback]] == Container Conditional Rollback diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-0-since-1-7.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-0-since-1-7.adoc index 9355130498..92fd10598b 100644 --- a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-0-since-1-7.adoc +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-0-since-1-7.adoc @@ -68,7 +68,7 @@ See xref:amqp/request-reply.adoc#async-template[Async Rabbit Template] for more The `RabbitTemplate` and `AsyncRabbitTemplate` now have `receiveAndConvert` and `convertSendAndReceiveAsType` methods that take a `ParameterizedTypeReference` argument, letting the caller specify the type to which to convert the result. This is particularly useful for complex types or when type information is not conveyed in message headers. It requires a `SmartMessageConverter` such as the `Jackson2JsonMessageConverter`. -See xref:amqp/request-reply.adoc[Request/Reply Messaging], xref:amqp/request-reply.adoc#async-template[Async Rabbit Template], xref:amqp/message-converters.adoc#json-complex[Converting From a `Message` With `RabbitTemplate`], and <> for more information. +See xref:amqp/request-reply.adoc[Request/Reply Messaging], xref:amqp/request-reply.adoc#async-template[Async Rabbit Template], xref:amqp/message-converters.adoc#json-complex[Converting From a `Message` With `RabbitTemplate`], and xref:amqp/message-converters.adoc#json-complex[Converting From a `Message` With `RabbitTemplate`] for more information. You can now use a `RabbitTemplate` to perform multiple operations on a dedicated channel. See xref:amqp/template.adoc#scoped-operations[Scoped Operations] for more information. @@ -179,7 +179,7 @@ See xref:amqp/transactions.adoc#conditional-rollback[Conditional Rollback] for m Deprecated in previous versions, Jackson `1.x` converters and related components have now been deleted. You can use similar components based on Jackson 2.x. -See <> for more information. +See xref:amqp/message-converters.adoc#json-message-converter[`Jackson2JsonMessageConverter`] for more information. [[json-message-converter]] == JSON Message Converter diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-1-since-2-0.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-1-since-2-0.adoc index a905c1899b..07f3bacc9c 100644 --- a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-1-since-2-0.adoc +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-1-since-2-0.adoc @@ -81,7 +81,7 @@ See xref:amqp/message-converters.adoc#jackson2xml[`Jackson2XmlMessageConverter`] == Management REST API The `RabbitManagementTemplate` is now deprecated in favor of the direct `com.rabbitmq.http.client.Client` (or `com.rabbitmq.http.client.ReactorNettyClient`) usage. -See <> for more information. +See xref:amqp/management-rest-api.adoc#management-rest-api[RabbitMQ REST API] for more information. [[rabbitlistener-changes]] == `@RabbitListener` Changes diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-to-1-2-since-1-1.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-to-1-2-since-1-1.adoc index 17410e9823..4180c54d78 100644 --- a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-to-1-2-since-1-1.adoc +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-to-1-2-since-1-1.adoc @@ -47,7 +47,7 @@ See xref:amqp/broker-configuration.adoc#conditional-declaration[Conditional Decl == AMQP Remoting Facilities are now provided for using Spring remoting techniques, using AMQP as the transport for the RPC calls. -For more information see <> +For more information see xref:amqp/request-reply.adoc#remoting[Spring Remoting with AMQP]. [[requested-heart-beats]] == Requested Heart Beats diff --git a/src/reference/antora/modules/ROOT/pages/integration-reference.adoc b/src/reference/antora/modules/ROOT/pages/integration-reference.adoc index 77745b8955..1dbc762022 100644 --- a/src/reference/antora/modules/ROOT/pages/integration-reference.adoc +++ b/src/reference/antora/modules/ROOT/pages/integration-reference.adoc @@ -13,7 +13,7 @@ We provide an inbound-channel-adapter, an outbound-channel-adapter, an inbound-g Since the AMQP adapters are part of the Spring Integration release, the documentation is available as part of the Spring Integration distribution. We provide a quick overview of the main features here. -See the https://docs.spring.io/spring-integration/reference[Spring Integration Reference Guide] for much more detail. +See the {spring-integration-docs}[Spring Integration Reference Guide] for much more detail. [[inbound-channel-adapter]] == Inbound Channel Adapter diff --git a/src/reference/antora/modules/ROOT/pages/logging.adoc b/src/reference/antora/modules/ROOT/pages/logging.adoc index 21b6855381..cf9b0ff0e5 100644 --- a/src/reference/antora/modules/ROOT/pages/logging.adoc +++ b/src/reference/antora/modules/ROOT/pages/logging.adoc @@ -395,7 +395,7 @@ public class MyEnhancedAppender extends AmqpAppender { } ---- -The Log4j 2 appender supports using a https://logging.apache.org/log4j/2.x/manual/appenders.html#BlockingQueueFactory[`BlockingQueueFactory`], as the following example shows: +The Log4j 2 appender supports using a https://logging.apache.org/log4j/2.x/manual/appenders/delegating.html#BlockingQueueFactory[`BlockingQueueFactory`], as the following example shows: [source, xml] ---- diff --git a/src/reference/antora/modules/ROOT/pages/sample-apps.adoc b/src/reference/antora/modules/ROOT/pages/sample-apps.adoc index bf983d5edc..90a2775e53 100644 --- a/src/reference/antora/modules/ROOT/pages/sample-apps.adoc +++ b/src/reference/antora/modules/ROOT/pages/sample-apps.adoc @@ -20,7 +20,7 @@ You can import the `spring-rabbit-helloworld` sample into the IDE and then follo Within the `src/main/java` directory, navigate to the `org.springframework.amqp.helloworld` package. Open the `HelloWorldConfiguration` class and notice that it contains the `@Configuration` annotation at the class level and notice some `@Bean` annotations at method-level. This is an example of Spring's Java-based configuration. -You can read more about that https://docs.spring.io/spring/docs/current/spring-framework-reference/html/beans.html#beans-java[here]. +You can read more about that {spring-framework-docs}/core/beans/java.html[here]. The following listing shows how the connection factory is created: @@ -144,7 +144,7 @@ static class ScheduledProducer { ---- You do not need to understand all of the details, since the real focus should be on the receiving side (which we cover next). -However, if you are not yet familiar with Spring task scheduling support, you can learn more https://docs.spring.io/spring/docs/current/spring-framework-reference/html/scheduling.html#scheduling-annotation-support[here]. +However, if you are not yet familiar with Spring task scheduling support, you can learn more {spring-framework-docs}/integration/scheduling.html#scheduling-annotation-support-scheduled[here]. The short story is that the `postProcessor` bean in the `ProducerConfiguration` registers the task with a scheduler. Now we can turn to the receiving side. @@ -351,4 +351,4 @@ Spring applications, when sending JSON, set the `__TypeId__` header to the fully The `spring-rabbit-json` sample explores several techniques to convert the JSON from a non-Spring application. -See also xref:amqp/message-converters.adoc#json-message-converter[`Jackson2JsonMessageConverter`] as well as the https://docs.spring.io/spring-amqp/docs/current/api/index.html?org/springframework/amqp/support/converter/DefaultClassMapper.html[Javadoc for the `DefaultClassMapper`]. +See also xref:amqp/message-converters.adoc#json-message-converter[`Jackson2JsonMessageConverter`] as well as the {spring-amqp-java-docs}/index.html?org/springframework/amqp/support/converter/DefaultClassMapper.html[Javadoc for the `DefaultClassMapper`]. diff --git a/src/reference/antora/modules/ROOT/pages/stream.adoc b/src/reference/antora/modules/ROOT/pages/stream.adoc index 4994354fca..8943b31695 100644 --- a/src/reference/antora/modules/ROOT/pages/stream.adoc +++ b/src/reference/antora/modules/ROOT/pages/stream.adoc @@ -109,7 +109,7 @@ You can also send native stream `Message` s directly; with the `messageBuilder() The `ProducerCustomizer` provides a mechanism to customize the producer before it is built. -Refer to the https://rabbitmq.github.io/rabbitmq-stream-java-client/stable/htmlsingle/[Java Client Documentation] about customizing the `Environment` and `Producer`. +Refer to the {rabbitmq-stream-docs}[Java Client Documentation] about customizing the `Environment` and `Producer`. IMPORTANT: Starting with version 3.0, the method return types are `CompletableFuture` instead of `ListenableFuture`. @@ -135,7 +135,7 @@ See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration] Similar the template, the container has a `ConsumerCustomizer` property. -Refer to the https://rabbitmq.github.io/rabbitmq-stream-java-client/stable/htmlsingle/[Java Client Documentation] about customizing the `Environment` and `Consumer`. +Refer to the {rabbitmq-stream-docs}[Java Client Documentation] about customizing the `Environment` and `Consumer`. When using `@RabbitListener`, configure a `StreamRabbitListenerContainerFactory`; at this time, most `@RabbitListener` properties (`concurrency`, etc) are ignored. Only `id`, `queues`, `autoStartup` and `containerFactory` are supported. In addition, `queues` can only contain one stream name. @@ -287,7 +287,7 @@ StreamListenerContainer container(Environment env, String name) { ---- IMPORTANT: At this time, when the concurrency is greater than 1, the actual concurrency is further controlled by the `Environment`; to achieve full concurrency, set the environment's `maxConsumersByConnection` to 1. -See https://rabbitmq.github.io/rabbitmq-stream-java-client/snapshot/htmlsingle/#configuring-the-environment[Configuring the Environment]. +See {rabbitmq-stream-docs}/#configuring-the-environment[Configuring the Environment]. [[stream-micrometer-observation]] == Micrometer Observation @@ -298,7 +298,7 @@ The container now also supports Micrometer timers (when observation is not enabl Set `observationEnabled` on each component to enable observation; this will disable xref:amqp/receiving-messages/micrometer.adoc[Micrometer Timers] because the timers will now be managed with each observation. When using annotated listeners, set `observationEnabled` on the container factory. -Refer to https://docs.micrometer.io/tracing/reference/[Micrometer Tracing] for more information. +Refer to {micrometer-tracing-docs}[Micrometer Tracing] for more information. To add tags to timers/traces, configure a custom `RabbitStreamTemplateObservationConvention` or `RabbitStreamListenerObservationConvention` to the template or listener container, respectively. From 2a634227703ef57adbe4a8994fbd08066108b273 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 16 Sep 2024 14:52:52 -0400 Subject: [PATCH 544/737] Remove explicit `com.gradle.develocity` plugin It is managed now transitively by the `io.spring.develocity.conventions` **Auto-cherry-pick to `3.1.x`** --- settings.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index 5c20e31b03..87c12dd0fa 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,7 +6,6 @@ pluginManagement { } plugins { - id 'com.gradle.develocity' version '3.17.6' id 'io.spring.develocity.conventions' version '0.0.21' } From 308841b1cd5e008624aad774abe2f2eedba2ca58 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 16 Sep 2024 15:05:20 -0400 Subject: [PATCH 545/737] Upgrade deps to milestones; prepare for release * Update to the latest available third-party dependencies --- build.gradle | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index fddd060b39..ae0a260db7 100644 --- a/build.gradle +++ b/build.gradle @@ -48,7 +48,7 @@ ext { assertjVersion = '3.26.3' assertkVersion = '0.28.1' awaitilityVersion = '4.2.2' - commonsCompressVersion = '1.26.2' + commonsCompressVersion = '1.27.1' commonsHttpClientVersion = '5.3.1' commonsPoolVersion = '2.12.0' hamcrestVersion = '2.2' @@ -58,21 +58,21 @@ ext { junit4Version = '4.13.2' junitJupiterVersion = '5.11.0' kotlinCoroutinesVersion = '1.8.1' - log4jVersion = '2.23.1' + log4jVersion = '2.24.0' logbackVersion = '1.5.8' lz4Version = '1.8.0' micrometerDocsVersion = '1.0.4' - micrometerVersion = '1.14.0-SNAPSHOT' - micrometerTracingVersion = '1.4.0-SNAPSHOT' - mockitoVersion = '5.12.0' + micrometerVersion = '1.14.0-M3' + micrometerTracingVersion = '1.4.0-M3' + mockitoVersion = '5.13.0' rabbitmqStreamVersion = '0.15.0' rabbitmqVersion = '5.21.0' - reactorVersion = '2024.0.0-SNAPSHOT' + reactorVersion = '2024.0.0-M6' snappyVersion = '1.1.10.7' springDataVersion = '2024.0.4' springRetryVersion = '2.0.9' - springVersion = '6.2.0-SNAPSHOT' - testcontainersVersion = '1.19.8' + springVersion = '6.2.0-RC1' + testcontainersVersion = '1.20.1' zstdJniVersion = '1.5.6-5' javaProjects = subprojects - project(':spring-amqp-bom') From 1e188c3b8828af90ed78acdd2f6fdae7c8d98252 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 16 Sep 2024 15:33:54 -0400 Subject: [PATCH 546/737] Fix link in the `changes-in-1-3-since-1-2.adoc` --- .../appendix/previous-whats-new/changes-in-1-3-since-1-2.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-3-since-1-2.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-3-since-1-2.adoc index 7aa1b869ff..5c5948f7d2 100644 --- a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-3-since-1-2.adoc +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-3-since-1-2.adoc @@ -47,7 +47,7 @@ If you wish to bind with an empty string routing key, you need to specify `key=" The `AmqpTemplate` now provides several synchronous `receiveAndReply` methods. These are implemented by the `RabbitTemplate`. -For more information see xref:amqp/receiving-messages.adoct[Receiving Messages]. +For more information see xref:amqp/receiving-messages.adoc[Receiving Messages]. The `RabbitTemplate` now supports configuring a `RetryTemplate` to attempt retries (with optional back-off policy) for when the broker is not available. For more information see xref:amqp/template.adoc#template-retry[Adding Retry Capabilities]. From 98d9df89b34f1037ee5bd10bbacd857c60640804 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 16 Sep 2024 19:40:00 +0000 Subject: [PATCH 547/737] [artifactory-release] Release version 3.2.0-M3 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 6bc0422ab1..b000448eff 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.2.0-SNAPSHOT +version=3.2.0-M3 org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true From 52ff8d1257608de070f19d7ac060f986996a577f Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 16 Sep 2024 19:40:02 +0000 Subject: [PATCH 548/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index b000448eff..6bc0422ab1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.2.0-M3 +version=3.2.0-SNAPSHOT org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true From b4339b3c615c748264b1c5188c62028234eba591 Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Tue, 17 Sep 2024 22:52:12 +0700 Subject: [PATCH 549/737] More link fixes in docs --- src/reference/antora/antora.yml | 4 +++- .../antora/modules/ROOT/pages/amqp/abstractions.adoc | 2 +- src/reference/antora/modules/ROOT/pages/amqp/connections.adoc | 2 +- .../antora/modules/ROOT/pages/amqp/containerAttributes.adoc | 2 +- .../antora/modules/ROOT/pages/amqp/management-rest-api.adoc | 2 +- .../appendix/previous-whats-new/changes-in-1-3-since-1-2.adoc | 2 +- .../appendix/previous-whats-new/changes-in-1-4-since-1-3.adoc | 4 ++-- .../appendix/previous-whats-new/changes-in-2-3-since-2-2.adoc | 2 +- src/reference/antora/modules/ROOT/pages/stream.adoc | 2 +- 9 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/reference/antora/antora.yml b/src/reference/antora/antora.yml index f3ceb248f4..a3fb163dea 100644 --- a/src/reference/antora/antora.yml +++ b/src/reference/antora/antora.yml @@ -25,4 +25,6 @@ asciidoc: micrometer-docs: 'https://docs.micrometer.io' micrometer-tracing-docs: '{micrometer-docs}/tracing/reference/' micrometer-micrometer-docs: '{micrometer-docs}/micrometer/reference/' - rabbitmq-stream-docs: 'https://rabbitmq.github.io/rabbitmq-stream-java-client/stable/htmlsingle' \ No newline at end of file + rabbitmq-stream-docs: 'https://rabbitmq.github.io/rabbitmq-stream-java-client/stable/htmlsingle' + rabbitmq-github: 'https://github.com/rabbitmq' + rabbitmq-server-github: '{rabbitmq-github}/rabbitmq-server/tree/main/deps' \ No newline at end of file diff --git a/src/reference/antora/modules/ROOT/pages/amqp/abstractions.adoc b/src/reference/antora/modules/ROOT/pages/amqp/abstractions.adoc index b86178a244..9d7bad429f 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/abstractions.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/abstractions.adoc @@ -93,7 +93,7 @@ Starting with version 3.2, the `ConsistentHashExchange` type has been introduced It provided options like `x-consistent-hash` for an exchange type. Allows to configure `hash-header` or `hash-property` exchange definition argument. The respective RabbitMQ `rabbitmq_consistent_hash_exchange` plugin has to be enabled on the broker. -More information about the purpose, logic and behavior of the Consistent Hash Exchange are in the official RabbitMQ https://github.com/rabbitmq/rabbitmq-server/tree/main/deps/rabbitmq_consistent_hash_exchange[documentation]. +More information about the purpose, logic and behavior of the Consistent Hash Exchange are in the official RabbitMQ {rabbitmq-server-github}/rabbitmq_consistent_hash_exchange[documentation]. NOTE: The AMQP specification also requires that any broker provide a "`default`" direct exchange that has no name. All queues that are declared are bound to that default `Exchange` with their names as routing keys. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc b/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc index da8bf568b8..c1b278a820 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc @@ -429,7 +429,7 @@ Starting with version 3.0, the underlying connection factory will attempt to con To revert to the previous behavior of attempting to connect from first to last, set the `addressShuffleMode` property to `AddressShuffleMode.NONE`. Starting with version 2.3, the `INORDER` shuffle mode was added, which means the first address is moved to the end after a connection is created. -You may wish to use this mode with the https://github.com/rabbitmq/rabbitmq-sharding[RabbitMQ Sharding Plugin] with `CacheMode.CONNECTION` and suitable concurrency if you wish to consume from all shards on all nodes. +You may wish to use this mode with the {rabbitmq-server-github}/rabbitmq_sharding[RabbitMQ Sharding Plugin] with `CacheMode.CONNECTION` and suitable concurrency if you wish to consume from all shards on all nodes. [source, java] ---- diff --git a/src/reference/antora/modules/ROOT/pages/amqp/containerAttributes.adoc b/src/reference/antora/modules/ROOT/pages/amqp/containerAttributes.adoc index 9aec7d7c9b..f876898405 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/containerAttributes.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/containerAttributes.adoc @@ -250,7 +250,7 @@ a| |[[consumeDelay]]<> + (N/A) -|When using the https://github.com/rabbitmq/rabbitmq-sharding[RabbitMQ Sharding Plugin] with `concurrentConsumers > 1`, there is a race condition that can prevent even distribution of the consumers across the shards. +|When using the {rabbitmq-server-github}/rabbitmq_sharding[RabbitMQ Sharding Plugin] with `concurrentConsumers > 1`, there is a race condition that can prevent even distribution of the consumers across the shards. Use this property to add a small delay between consumer starts to avoid this race condition. You should experiment with values to determine the suitable delay for your environment. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/management-rest-api.adoc b/src/reference/antora/modules/ROOT/pages/amqp/management-rest-api.adoc index a693e1e36d..628c0ff7b1 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/management-rest-api.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/management-rest-api.adoc @@ -3,7 +3,7 @@ :page-section-summary-toc: 1 When the management plugin is enabled, the RabbitMQ server exposes a REST API to monitor and configure the broker. -A https://github.com/rabbitmq/hop[Java Binding for the API] is now provided. +A {rabbitmq-github}/hop[Java Binding for the API] is now provided. The `com.rabbitmq.http.client.Client` is a standard, immediate, and, therefore, blocking API. It is based on the {spring-framework-docs}/web.html[Spring Web] module and its `RestTemplate` implementation. On the other hand, the `com.rabbitmq.http.client.ReactorNettyClient` is a reactive, non-blocking implementation based on the https://projectreactor.io/docs/netty/release/reference/docs/index.html[Reactor Netty] project. diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-3-since-1-2.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-3-since-1-2.adoc index 5c5948f7d2..41ddf37416 100644 --- a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-3-since-1-2.adoc +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-3-since-1-2.adoc @@ -70,7 +70,7 @@ See xref:amqp/broker-configuration.adoc#headers-exchange[Headers Exchange]. A new `SimpleRoutingConnectionFactory` has been introduced. It allows configuration of `ConnectionFactories` mapping, to determine the target `ConnectionFactory` to use at runtime. -See xref:amqp/connections.adoc#routing-connection-factory[routing-connection-factory]. +See xref:amqp/connections.adoc#routing-connection-factory[Routing Connection Factory]. [[messagebuilder-and-messagepropertiesbuilder]] == `MessageBuilder` and `MessagePropertiesBuilder` diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-4-since-1-3.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-4-since-1-3.adoc index c43098a182..fef889d846 100644 --- a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-4-since-1-3.adoc +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-4-since-1-3.adoc @@ -71,13 +71,13 @@ The `mandatoryExpression`, `sendConnectionFactorySelectorExpression`, and `recei The `mandatoryExpression` is used to evaluate a `mandatory` boolean value against each request message when a `ReturnCallback` is in use. See xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns]. The `sendConnectionFactorySelectorExpression` and `receiveConnectionFactorySelectorExpression` are used when an `AbstractRoutingConnectionFactory` is provided, to determine the `lookupKey` for the target `ConnectionFactory` at runtime on each AMQP protocol interaction operation. -See xref:amqp/connections.adoc#routing-connection-factory[routing-connection-factory]. +See xref:amqp/connections.adoc#routing-connection-factory[Routing Connection Factory]. [[listeners-and-the-routing-connection-factory]] == Listeners and the Routing Connection Factory You can configure a `SimpleMessageListenerContainer` with a routing connection factory to enable connection selection based on the queue names. -See xref:amqp/connections.adoc#routing-connection-factory[routing-connection-factory]. +See xref:amqp/connections.adoc#routing-connection-factory[Routing Connection Factory]. [[rabbittemplate:-recoverycallback-option]] == `RabbitTemplate`: `RecoveryCallback` Option diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-3-since-2-2.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-3-since-2-2.adoc index d49bf3128f..fca45e9164 100644 --- a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-3-since-2-2.adoc +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-3-since-2-2.adoc @@ -43,7 +43,7 @@ See xref:amqp/request-reply.adoc#direct-reply-to[RabbitMQ Direct reply-to] for m [[listener-container-changes]] == Listener Container Changes -A new listener container property `consumeDelay` is now available; it is helpful when using the https://github.com/rabbitmq/rabbitmq-sharding[RabbitMQ Sharding Plugin]. +A new listener container property `consumeDelay` is now available; it is helpful when using the {rabbitmq-server-github}/rabbitmq_sharding[RabbitMQ Sharding Plugin]. The default `JavaLangErrorHandler` now calls `System.exit(99)`. To revert to the previous behavior (do nothing), add a no-op handler. diff --git a/src/reference/antora/modules/ROOT/pages/stream.adoc b/src/reference/antora/modules/ROOT/pages/stream.adoc index 8943b31695..e9f5119e03 100644 --- a/src/reference/antora/modules/ROOT/pages/stream.adoc +++ b/src/reference/antora/modules/ROOT/pages/stream.adoc @@ -1,7 +1,7 @@ [[stream-support]] = Using the RabbitMQ Stream Plugin -Version 2.4 introduces initial support for the https://github.com/rabbitmq/rabbitmq-stream-java-client[RabbitMQ Stream Plugin Java Client] for the https://rabbitmq.com/stream.html[RabbitMQ Stream Plugin]. +Version 2.4 introduces initial support for the {rabbitmq-github}/rabbitmq-stream-java-client[RabbitMQ Stream Plugin Java Client] for the https://rabbitmq.com/stream.html[RabbitMQ Stream Plugin]. * `RabbitStreamTemplate` * `StreamListenerContainer` From aa21c73500085fe50bbd62ac05f8df5cd1a1e6e4 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 18 Sep 2024 16:27:36 -0400 Subject: [PATCH 550/737] Migrate to `DEVELOCITY_ACCESS_KEY` secret for GHA workflows **Auto-cherry-pick to `3.1.x`** --- .github/workflows/ci-snapshot.yml | 4 ++-- .github/workflows/release.yml | 2 +- .github/workflows/verify-staged-artifacts.yml | 4 +--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-snapshot.yml b/.github/workflows/ci-snapshot.yml index c57e8778e7..a7191a1997 100644 --- a/.github/workflows/ci-snapshot.yml +++ b/.github/workflows/ci-snapshot.yml @@ -17,10 +17,10 @@ concurrency: jobs: build-snapshot: - uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-snapshot.yml@v3 + uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-snapshot.yml@main with: gradleTasks: ${{ github.event_name == 'schedule' && '--rerun-tasks' || '' }} secrets: - GRADLE_ENTERPRISE_SECRET_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c0bea04ccc..c2a43c3e96 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-release.yml@main secrets: GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} - GRADLE_ENTERPRISE_SECRET_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} JF_ARTIFACTORY_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} diff --git a/.github/workflows/verify-staged-artifacts.yml b/.github/workflows/verify-staged-artifacts.yml index bee9abc943..979726f519 100644 --- a/.github/workflows/verify-staged-artifacts.yml +++ b/.github/workflows/verify-staged-artifacts.yml @@ -10,9 +10,7 @@ on: env: - GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} - GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} - GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} From ed0d7905ac74f13a7c02433e545010dcc6458e4e Mon Sep 17 00:00:00 2001 From: vmeunier Date: Thu, 19 Sep 2024 21:24:33 +0200 Subject: [PATCH 551/737] GH-2814: OTel tags for RabbitTemplate and RabbitListener Fixes: #2814 Issue link: https://github.com/spring-projects/spring-amqp/issues/2814 * Add Opentelemetry tags `RabbitTemplate ` * Add Opentelemetry tags `RabbitListenerObservationConvention` --- .../micrometer/RabbitListenerObservation.java | 43 +++++++++++++-- .../micrometer/RabbitTemplateObservation.java | 37 ++++++++++++- .../ObservationIntegrationTests.java | 55 ++++++++++++++----- 3 files changed, 113 insertions(+), 22 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java index b580bd2e7e..f5f8528491 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2024 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. @@ -26,8 +26,8 @@ * Spring Rabbit Observation for listeners. * * @author Gary Russell + * @author Vincent Meunier * @since 3.0 - * */ public enum RabbitListenerObservation implements ObservationDocumentation { @@ -36,7 +36,6 @@ public enum RabbitListenerObservation implements ObservationDocumentation { */ LISTENER_OBSERVATION { - @Override public Class> getDefaultConvention() { return DefaultRabbitListenerObservationConvention.class; @@ -69,6 +68,34 @@ public String asString() { return "spring.rabbit.listener.id"; } + }, + + /** + * The queue the listener is plugged to. + * + * @since 3.2 + */ + DESTINATION_NAME { + + @Override + public String asString() { + return "messaging.destination.name"; + } + + }, + + /** + * The delivery tag. + * + * @since 3.2 + */ + DELIVERY_TAG { + + @Override + public String asString() { + return "messaging.rabbitmq.message.delivery_tag"; + } + } } @@ -86,8 +113,14 @@ public static class DefaultRabbitListenerObservationConvention implements Rabbit @Override public KeyValues getLowCardinalityKeyValues(RabbitMessageReceiverContext context) { - return KeyValues.of(RabbitListenerObservation.ListenerLowCardinalityTags.LISTENER_ID.asString(), - context.getListenerId()); + final var messageProperties = context.getCarrier().getMessageProperties(); + return KeyValues.of( + RabbitListenerObservation.ListenerLowCardinalityTags.LISTENER_ID.asString(), context.getListenerId(), + RabbitListenerObservation.ListenerLowCardinalityTags.DESTINATION_NAME.asString(), + messageProperties.getConsumerQueue(), + RabbitListenerObservation.ListenerLowCardinalityTags.DELIVERY_TAG.asString(), + String.valueOf(messageProperties.getDeliveryTag()) + ); } @Override diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservation.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservation.java index f3bc17f1e6..6008e1e06d 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservation.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservation.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2024 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. @@ -26,6 +26,7 @@ * Spring RabbitMQ Observation for {@link org.springframework.amqp.rabbit.core.RabbitTemplate}. * * @author Gary Russell + * @author Vincent Meunier * @since 3.0 * */ @@ -68,8 +69,35 @@ public String asString() { return "spring.rabbit.template.name"; } + }, + + /** + * The destination exchange (empty if default exchange). + * @since 3.2 + */ + EXCHANGE { + + @Override + public String asString() { + return "messaging.destination.name"; + } + + }, + + /** + * The destination routing key. + * @since 3.2 + */ + ROUTING_KEY { + + @Override + public String asString() { + return "messaging.rabbitmq.destination.routing_key"; + } + } + } /** @@ -85,8 +113,11 @@ public static class DefaultRabbitTemplateObservationConvention implements Rabbit @Override public KeyValues getLowCardinalityKeyValues(RabbitMessageSenderContext context) { - return KeyValues.of(RabbitTemplateObservation.TemplateLowCardinalityTags.BEAN_NAME.asString(), - context.getBeanName()); + return KeyValues.of( + TemplateLowCardinalityTags.BEAN_NAME.asString(), context.getBeanName(), + TemplateLowCardinalityTags.EXCHANGE.asString(), context.getExchange(), + TemplateLowCardinalityTags.ROUTING_KEY.asString(), context.getRoutingKey() + ); } @Override diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java index 41c7facb86..920deec403 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2024 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. @@ -21,7 +21,6 @@ import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.EnableRabbit; @@ -35,6 +34,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import io.micrometer.common.KeyValue; import io.micrometer.common.KeyValues; import io.micrometer.core.tck.MeterRegistryAssert; import io.micrometer.observation.ObservationRegistry; @@ -47,7 +47,6 @@ /** * @author Artem Bilan * @author Gary Russell - * * @since 3.0 */ @RabbitAvailable(queues = { "int.observation.testQ1", "int.observation.testQ2" }) @@ -72,40 +71,69 @@ public SampleTestRunnerConsumer yourCode() { .hasSize(4); List producerSpans = finishedSpans.stream() .filter(span -> span.getKind().equals(Kind.PRODUCER)) - .collect(Collectors.toList()); + .toList(); List consumerSpans = finishedSpans.stream() .filter(span -> span.getKind().equals(Kind.CONSUMER)) - .collect(Collectors.toList()); + .toList(); SpanAssert.assertThat(producerSpans.get(0)) - .hasTag("spring.rabbit.template.name", "template"); + .hasTag("spring.rabbit.template.name", "template") + .hasTag("messaging.destination.name", "") + .hasTag("messaging.rabbitmq.destination.routing_key", "int.observation.testQ1"); SpanAssert.assertThat(producerSpans.get(0)) .hasRemoteServiceNameEqualTo("RabbitMQ"); SpanAssert.assertThat(producerSpans.get(1)) - .hasTag("spring.rabbit.template.name", "template"); + .hasTag("spring.rabbit.template.name", "template") + .hasTag("messaging.destination.name", "") + .hasTag("messaging.rabbitmq.destination.routing_key", "int.observation.testQ2"); SpanAssert.assertThat(consumerSpans.get(0)) - .hasTagWithKey("spring.rabbit.listener.id"); + .hasTagWithKey("spring.rabbit.listener.id") + .hasTag("messaging.destination.name", "int.observation.testQ1") + .hasTag("messaging.rabbitmq.message.delivery_tag", "1"); SpanAssert.assertThat(consumerSpans.get(0)) .hasRemoteServiceNameEqualTo("RabbitMQ"); assertThat(consumerSpans.get(0).getTags().get("spring.rabbit.listener.id")).isIn("obs1", "obs2"); SpanAssert.assertThat(consumerSpans.get(1)) .hasTagWithKey("spring.rabbit.listener.id"); assertThat(consumerSpans.get(1).getTags().get("spring.rabbit.listener.id")).isIn("obs1", "obs2"); + SpanAssert.assertThat(consumerSpans.get(1)) + .hasTagWithKey("spring.rabbit.listener.id") + .hasTag("messaging.destination.name", "int.observation.testQ2") + .hasTag("messaging.rabbitmq.message.delivery_tag", "1"); assertThat(consumerSpans.get(0).getTags().get("spring.rabbit.listener.id")) .isNotEqualTo(consumerSpans.get(1).getTags().get("spring.rabbit.listener.id")); MeterRegistryAssert.assertThat(getMeterRegistry()) .hasTimerWithNameAndTags("spring.rabbit.template", - KeyValues.of("spring.rabbit.template.name", "template")) + KeyValues.of( + KeyValue.of("spring.rabbit.template.name", "template"), + KeyValue.of("messaging.destination.name", ""), + KeyValue.of("messaging.rabbitmq.destination.routing_key", "int.observation.testQ1") + ) + ) .hasTimerWithNameAndTags("spring.rabbit.template", - KeyValues.of("spring.rabbit.template.name", "template")) + KeyValues.of( + KeyValue.of("spring.rabbit.template.name", "template"), + KeyValue.of("messaging.destination.name", ""), + KeyValue.of("messaging.rabbitmq.destination.routing_key", "int.observation.testQ2") + ) + ) .hasTimerWithNameAndTags("spring.rabbit.listener", - KeyValues.of("spring.rabbit.listener.id", "obs1")) + KeyValues.of( + KeyValue.of("spring.rabbit.listener.id", "obs1"), + KeyValue.of("messaging.destination.name", "int.observation.testQ1"), + KeyValue.of("messaging.rabbitmq.message.delivery_tag", "1") + ) + ) .hasTimerWithNameAndTags("spring.rabbit.listener", - KeyValues.of("spring.rabbit.listener.id", "obs2")); + KeyValues.of( + KeyValue.of("spring.rabbit.listener.id", "obs2"), + KeyValue.of("messaging.destination.name", "int.observation.testQ2"), + KeyValue.of("messaging.rabbitmq.message.delivery_tag", "1") + ) + ); }; } - @Configuration @EnableRabbit public static class Config { @@ -159,5 +187,4 @@ void listen2(Message in) { } - } From ed8f13ce4564129bc35373451c3fcc365176745f Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 20 Sep 2024 13:52:36 -0400 Subject: [PATCH 552/737] Migrate to `DEVELOCITY_ACCESS_KEY` secret for GHA **Auto-cherry-pick to `3.1.x`** --- .github/workflows/ci-snapshot.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/verify-staged-artifacts.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-snapshot.yml b/.github/workflows/ci-snapshot.yml index a7191a1997..a21192cd2c 100644 --- a/.github/workflows/ci-snapshot.yml +++ b/.github/workflows/ci-snapshot.yml @@ -21,6 +21,6 @@ jobs: with: gradleTasks: ${{ github.event_name == 'schedule' && '--rerun-tasks' || '' }} secrets: - DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c2a43c3e96..90f17f1609 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-release.yml@main secrets: GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} - DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} JF_ARTIFACTORY_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} diff --git a/.github/workflows/verify-staged-artifacts.yml b/.github/workflows/verify-staged-artifacts.yml index 979726f519..9a83a2024d 100644 --- a/.github/workflows/verify-staged-artifacts.yml +++ b/.github/workflows/verify-staged-artifacts.yml @@ -10,7 +10,7 @@ on: env: - DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} From 3111a601ae1d4f18cc52d0ec36dd4acab52cddc7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 21 Sep 2024 02:03:04 +0000 Subject: [PATCH 553/737] Bump io.micrometer:micrometer-bom from 1.14.0-M3 to 1.14.0-SNAPSHOT (#2834) Bumps [io.micrometer:micrometer-bom](https://github.com/micrometer-metrics/micrometer) from 1.14.0-M3 to 1.14.0-SNAPSHOT. - [Release notes](https://github.com/micrometer-metrics/micrometer/releases) - [Commits](https://github.com/micrometer-metrics/micrometer/commits) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ae0a260db7..d4a718976f 100644 --- a/build.gradle +++ b/build.gradle @@ -62,7 +62,7 @@ ext { logbackVersion = '1.5.8' lz4Version = '1.8.0' micrometerDocsVersion = '1.0.4' - micrometerVersion = '1.14.0-M3' + micrometerVersion = '1.14.0-SNAPSHOT' micrometerTracingVersion = '1.4.0-M3' mockitoVersion = '5.13.0' rabbitmqStreamVersion = '0.15.0' From 6b0c889fae85984e30cb9adeb5e22d09c80bf227 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 21 Sep 2024 02:05:06 +0000 Subject: [PATCH 554/737] Bump io.projectreactor:reactor-bom from 2024.0.0-M6 to 2024.0.0-SNAPSHOT (#2837) Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2024.0.0-M6 to 2024.0.0-SNAPSHOT. - [Release notes](https://github.com/reactor/reactor/releases) - [Commits](https://github.com/reactor/reactor/commits) --- updated-dependencies: - dependency-name: io.projectreactor:reactor-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d4a718976f..64779e3104 100644 --- a/build.gradle +++ b/build.gradle @@ -67,7 +67,7 @@ ext { mockitoVersion = '5.13.0' rabbitmqStreamVersion = '0.15.0' rabbitmqVersion = '5.21.0' - reactorVersion = '2024.0.0-M6' + reactorVersion = '2024.0.0-SNAPSHOT' snappyVersion = '1.1.10.7' springDataVersion = '2024.0.4' springRetryVersion = '2.0.9' From 8c714fa708f0140d58fabbdcece4b18d3bcda90b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 21 Sep 2024 02:05:26 +0000 Subject: [PATCH 555/737] Bump org.springframework:spring-framework-bom (#2835) Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.2.0-RC1 to 6.2.0-SNAPSHOT. - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/commits) --- updated-dependencies: - dependency-name: org.springframework:spring-framework-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 64779e3104..cec6251a24 100644 --- a/build.gradle +++ b/build.gradle @@ -71,7 +71,7 @@ ext { snappyVersion = '1.1.10.7' springDataVersion = '2024.0.4' springRetryVersion = '2.0.9' - springVersion = '6.2.0-RC1' + springVersion = '6.2.0-SNAPSHOT' testcontainersVersion = '1.20.1' zstdJniVersion = '1.5.6-5' From 872a56462f746e753c22717971191c72735ba269 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 21 Sep 2024 02:06:35 +0000 Subject: [PATCH 556/737] Bump io.micrometer:micrometer-tracing-bom (#2836) Bumps [io.micrometer:micrometer-tracing-bom](https://github.com/micrometer-metrics/tracing) from 1.4.0-M3 to 1.4.0-SNAPSHOT. - [Release notes](https://github.com/micrometer-metrics/tracing/releases) - [Commits](https://github.com/micrometer-metrics/tracing/commits) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-tracing-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index cec6251a24..284c3366ec 100644 --- a/build.gradle +++ b/build.gradle @@ -63,7 +63,7 @@ ext { lz4Version = '1.8.0' micrometerDocsVersion = '1.0.4' micrometerVersion = '1.14.0-SNAPSHOT' - micrometerTracingVersion = '1.4.0-M3' + micrometerTracingVersion = '1.4.0-SNAPSHOT' mockitoVersion = '5.13.0' rabbitmqStreamVersion = '0.15.0' rabbitmqVersion = '5.21.0' From 951781544a42e6e26ffe85d6eda621cff31aed9c Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Mon, 23 Sep 2024 21:58:06 +0700 Subject: [PATCH 557/737] Modernize code for diamond operator * Use `isEmpty()` instead for length for `String` or `Collection` --- .../springframework/amqp/core/AbstractBuilder.java | 5 +++-- .../springframework/amqp/core/AbstractDeclarable.java | 9 +++++---- .../org/springframework/amqp/core/BindingBuilder.java | 9 +++++---- .../amqp/support/SimpleAmqpHeaderMapper.java | 3 ++- .../support/converter/AbstractJavaTypeMapper.java | 7 ++++--- .../AllowedListDeserializingMessageConverter.java | 5 +++-- .../ContentTypeDelegatingMessageConverter.java | 5 +++-- .../converter/DefaultJackson2JavaTypeMapper.java | 5 +++-- .../DelegatingDecompressingPostProcessor.java | 5 +++-- .../postprocessor/MessagePostProcessorUtils.java | 11 ++++++----- .../org/springframework/amqp/utils/MapBuilder.java | 5 +++-- .../amqp/rabbit/AsyncRabbitTemplate.java | 7 ++++--- .../RabbitListenerAnnotationBeanPostProcessor.java | 4 ++-- .../amqp/rabbit/batch/SimpleBatchingStrategy.java | 5 +++-- .../amqp/rabbit/config/AbstractExchangeParser.java | 5 +++-- .../amqp/rabbit/config/NamespaceUtils.java | 5 +++-- .../connection/AbstractRoutingConnectionFactory.java | 7 ++++--- .../rabbit/connection/CompositeChannelListener.java | 7 ++++--- .../connection/CompositeConnectionListener.java | 7 ++++--- .../rabbit/connection/ConsumerChannelRegistry.java | 5 +++-- .../connection/LocalizedQueueConnectionFactory.java | 7 ++++--- .../connection/PublisherCallbackChannelImpl.java | 9 +++++---- .../amqp/rabbit/connection/RabbitResourceHolder.java | 5 +++-- .../amqp/rabbit/connection/SimpleResourceHolder.java | 9 +++++---- .../listener/AbstractRabbitListenerEndpoint.java | 5 +++-- .../listener/ConditionalRejectingErrorHandler.java | 5 +++-- .../listener/MultiMethodRabbitListenerEndpoint.java | 5 +++-- .../amqp/rabbit/support/ActiveObjectCounter.java | 7 ++++--- 28 files changed, 100 insertions(+), 73 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractBuilder.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractBuilder.java index fe09c2b6d6..30506f62ca 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractBuilder.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2024 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. @@ -23,6 +23,7 @@ * Base class for builders supporting arguments. * * @author Gary Russell + * @author Ngoc Nhan * @since 1.6 * */ @@ -36,7 +37,7 @@ public abstract class AbstractBuilder { */ protected Map getOrCreateArguments() { if (this.arguments == null) { - this.arguments = new LinkedHashMap(); + this.arguments = new LinkedHashMap<>(); } return this.arguments; } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractDeclarable.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractDeclarable.java index 83002a2ec3..678620e914 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractDeclarable.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractDeclarable.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -34,6 +34,7 @@ * * @author Gary Russell * @author Christian Tzolov + * @author Ngoc Nhan * @since 1.2 * */ @@ -43,7 +44,7 @@ public abstract class AbstractDeclarable implements Declarable { private boolean shouldDeclare = true; - private Collection declaringAdmins = new ArrayList(); + private Collection declaringAdmins = new ArrayList<>(); private boolean ignoreDeclarationExceptions; @@ -63,7 +64,7 @@ public AbstractDeclarable(@Nullable Map arguments) { this.arguments = new HashMap<>(arguments); } else { - this.arguments = new HashMap(); + this.arguments = new HashMap<>(); } } @@ -102,7 +103,7 @@ public void setIgnoreDeclarationExceptions(boolean ignoreDeclarationExceptions) @Override public void setAdminsThatShouldDeclare(Object... adminArgs) { - Collection admins = new ArrayList(); + Collection admins = new ArrayList<>(); if (adminArgs != null) { if (adminArgs.length > 1) { Assert.noNullElements(adminArgs, "'admins' cannot contain null elements"); diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/BindingBuilder.java b/spring-amqp/src/main/java/org/springframework/amqp/core/BindingBuilder.java index 642481750d..956d83d8ec 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/BindingBuilder.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/BindingBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -30,6 +30,7 @@ * @author Mark Fisher * @author Dave Syer * @author Gary Russell + * @author Ngoc Nhan */ public final class BindingBuilder { @@ -50,7 +51,7 @@ public static DestinationConfigurer bind(Exchange exchange) { } private static Map createMapForKeys(String... keys) { - Map map = new HashMap(); + Map map = new HashMap<>(); for (String key : keys) { map.put(key, null); } @@ -155,7 +156,7 @@ public Binding exists() { } public Binding matches(Object value) { - Map map = new HashMap(); + Map map = new HashMap<>(); map.put(this.key, value); return new Binding(HeadersExchangeMapConfigurer.this.destination.queue, HeadersExchangeMapConfigurer.this.destination.name, @@ -194,7 +195,7 @@ public final class HeadersExchangeMapBindingCreator { HeadersExchangeMapBindingCreator(Map headerMap, boolean matchAll) { Assert.notEmpty(headerMap, "header map must not be empty"); - this.headerMap = new HashMap(headerMap); + this.headerMap = new HashMap<>(headerMap); this.headerMap.put("x-match", (matchAll ? "all" : "any")); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/SimpleAmqpHeaderMapper.java b/spring-amqp/src/main/java/org/springframework/amqp/support/SimpleAmqpHeaderMapper.java index 2c9a35864b..2885f91f46 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/SimpleAmqpHeaderMapper.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/SimpleAmqpHeaderMapper.java @@ -49,6 +49,7 @@ * @author Artem Bilan * @author Stephane Nicoll * @author Raylax Grey + * @author Ngoc Nhan * @since 1.4 */ public class SimpleAmqpHeaderMapper extends AbstractHeaderMapper implements AmqpHeaderMapper { @@ -125,7 +126,7 @@ public void fromHeaders(MessageHeaders headers, MessageProperties amqpMessagePro @Override public MessageHeaders toHeaders(MessageProperties amqpMessageProperties) { - Map headers = new HashMap(); + Map headers = new HashMap<>(); try { BiConsumer putObject = headers::put; BiConsumer putString = headers::put; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJavaTypeMapper.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJavaTypeMapper.java index 4745f9623d..76531523be 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJavaTypeMapper.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJavaTypeMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -35,6 +35,7 @@ * @author Sam Nelson * @author Andreas Asplund * @author Gary Russell + * @author Ngoc Nhan */ public abstract class AbstractJavaTypeMapper implements BeanClassLoaderAware { @@ -44,9 +45,9 @@ public abstract class AbstractJavaTypeMapper implements BeanClassLoaderAware { public static final String DEFAULT_KEY_CLASSID_FIELD_NAME = "__KeyTypeId__"; - private final Map> idClassMapping = new HashMap>(); + private final Map> idClassMapping = new HashMap<>(); - private final Map, String> classIdMapping = new HashMap, String>(); + private final Map, String> classIdMapping = new HashMap<>(); private ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AllowedListDeserializingMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AllowedListDeserializingMessageConverter.java index 14d6247ddd..4f5e6d24ae 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AllowedListDeserializingMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AllowedListDeserializingMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2024 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. @@ -27,12 +27,13 @@ * MessageConverters that potentially use Java deserialization. * * @author Gary Russell + * @author Ngoc Nhan * @since 1.5.5 * */ public abstract class AllowedListDeserializingMessageConverter extends AbstractMessageConverter { - private final Set allowedListPatterns = new LinkedHashSet(); + private final Set allowedListPatterns = new LinkedHashSet<>(); /** * Set simple patterns for allowable packages/classes for deserialization. diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverter.java index 735d928ac5..0fad672ee1 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-2024 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. @@ -33,11 +33,12 @@ * @author Eric Rizzo * @author Gary Russell * @author Artem Bilan + * @author Ngoc Nhan * @since 1.4.2 */ public class ContentTypeDelegatingMessageConverter implements MessageConverter { - private final Map delegates = new HashMap(); + private final Map delegates = new HashMap<>(); private final MessageConverter defaultConverter; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapper.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapper.java index ed3423efa8..380892662b 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapper.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -36,6 +36,7 @@ * @author Andreas Asplund * @author Artem Bilan * @author Gary Russell + * @author Ngoc Nhan */ public class DefaultJackson2JavaTypeMapper extends AbstractJavaTypeMapper implements Jackson2JavaTypeMapper { @@ -45,7 +46,7 @@ public class DefaultJackson2JavaTypeMapper extends AbstractJavaTypeMapper implem "java.lang" ); - private final Set trustedPackages = new LinkedHashSet(TRUSTED_PACKAGES); + private final Set trustedPackages = new LinkedHashSet<>(TRUSTED_PACKAGES); private volatile TypePrecedence typePrecedence = TypePrecedence.INFERRED; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/DelegatingDecompressingPostProcessor.java b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/DelegatingDecompressingPostProcessor.java index e3e247f943..9474d1b7c8 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/DelegatingDecompressingPostProcessor.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/DelegatingDecompressingPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * Copyright 2014-2024 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. @@ -30,11 +30,12 @@ * * @author Gary Russell * @author David Diehl + * @author Ngoc Nhan * @since 1.4.2 */ public class DelegatingDecompressingPostProcessor implements MessagePostProcessor, Ordered { - private final Map decompressors = new HashMap(); + private final Map decompressors = new HashMap<>(); private int order; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/MessagePostProcessorUtils.java b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/MessagePostProcessorUtils.java index bf2ed742d8..2c7a8f8b05 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/MessagePostProcessorUtils.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/MessagePostProcessorUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2024 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. @@ -29,15 +29,16 @@ * Utilities for message post processors. * * @author Gary Russell + * @author Ngoc Nhan * @since 1.4.2 * */ public final class MessagePostProcessorUtils { public static Collection sort(Collection processors) { - List priorityOrdered = new ArrayList(); - List ordered = new ArrayList(); - List unOrdered = new ArrayList(); + List priorityOrdered = new ArrayList<>(); + List ordered = new ArrayList<>(); + List unOrdered = new ArrayList<>(); for (MessagePostProcessor processor : processors) { if (processor instanceof PriorityOrdered) { priorityOrdered.add(processor); @@ -49,7 +50,7 @@ else if (processor instanceof Ordered) { unOrdered.add(processor); } } - List sorted = new ArrayList(); + List sorted = new ArrayList<>(); OrderComparator.sort(priorityOrdered); sorted.addAll(priorityOrdered); OrderComparator.sort(ordered); diff --git a/spring-amqp/src/main/java/org/springframework/amqp/utils/MapBuilder.java b/spring-amqp/src/main/java/org/springframework/amqp/utils/MapBuilder.java index 1fea0e2d65..e3afc18117 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/utils/MapBuilder.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/utils/MapBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2024 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. @@ -26,11 +26,12 @@ * @param the value type. * @author Artem Bilan * @author Gary Russell + * @author Ngoc Nhan * @since 2.0 */ public class MapBuilder, K, V> { - private final Map map = new HashMap(); + private final Map map = new HashMap<>(); public B put(K key, V value) { this.map.put(key, value); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java index 077eacb7de..5bc5f44c89 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors. + * Copyright 2022-2024 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. @@ -88,6 +88,7 @@ * @author Gary Russell * @author Artem Bilan * @author FengYang Su + * @author Ngoc Nhan * * @since 1.6 */ @@ -482,7 +483,7 @@ public RabbitConverterFuture convertSendAndReceiveAsType(String exchange, private RabbitConverterFuture convertSendAndReceive(String exchange, String routingKey, Object object, MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) { - AsyncCorrelationData correlationData = new AsyncCorrelationData(messagePostProcessor, responseType, + AsyncCorrelationData correlationData = new AsyncCorrelationData<>(messagePostProcessor, responseType, this.enableConfirms); if (this.container != null) { this.template.convertAndSend(exchange, routingKey, object, this.messagePostProcessor, correlationData); @@ -731,7 +732,7 @@ public Message postProcessMessage(Message message, Correlation correlation) thro messageToSend = correlationData.userPostProcessor.postProcessMessage(message); } String correlationId = getOrSetCorrelationIdAndSetReplyTo(messageToSend, correlationData); - correlationData.future = new RabbitConverterFuture(correlationId, message, + correlationData.future = new RabbitConverterFuture<>(correlationId, message, AsyncRabbitTemplate.this::canceler, AsyncRabbitTemplate.this::timeoutTask); if (correlationData.enableConfirms) { correlationData.setId(correlationId); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java index 6946bb28e8..40f01b38bc 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java @@ -364,7 +364,7 @@ else if (source instanceof Method method) { private void processMultiMethodListeners(RabbitListener[] classLevelListeners, Method[] multiMethods, Object bean, String beanName) { - List checkedMethods = new ArrayList(); + List checkedMethods = new ArrayList<>(); Method defaultMethod = null; for (Method method : multiMethods) { Method checked = checkProxy(method, bean); @@ -734,7 +734,7 @@ else if (resolvedValueToUse instanceof Iterable) { } private String[] registerBeansForDeclaration(RabbitListener rabbitListener, Collection declarables) { - List queues = new ArrayList(); + List queues = new ArrayList<>(); if (this.beanFactory instanceof ConfigurableBeanFactory) { for (QueueBinding binding : rabbitListener.bindings()) { String queueName = declareQueue(binding.value(), declarables); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/SimpleBatchingStrategy.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/SimpleBatchingStrategy.java index d1558f7a10..8958cac06c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/SimpleBatchingStrategy.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/SimpleBatchingStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2024 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. @@ -39,6 +39,7 @@ * length field. * * @author Gary Russell + * @author Ngoc Nhan * @since 1.4.1 * */ @@ -50,7 +51,7 @@ public class SimpleBatchingStrategy implements BatchingStrategy { private final long timeout; - private final List messages = new ArrayList(); + private final List messages = new ArrayList<>(); private String exchange; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractExchangeParser.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractExchangeParser.java index 705fd84056..434e57d676 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractExchangeParser.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractExchangeParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -34,6 +34,7 @@ * @author Gary Russell * @author Felipe Gutierrez * @author Artem Bilan + * @author Ngoc Nhan * */ public abstract class AbstractExchangeParser extends AbstractSingleBeanDefinitionParser { @@ -144,7 +145,7 @@ private void parseArguments(Element element, String argumentsElementName, Parser Map map = parserContext.getDelegate().parseMapElement(argumentsElement, builder.getRawBeanDefinition()); if (StringUtils.hasText(ref)) { - if (map != null && map.size() > 0) { + if (map != null && !map.isEmpty()) { parserContext.getReaderContext().error("You cannot have both a 'ref' and a nested map", element); } if (propertyName == null) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/NamespaceUtils.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/NamespaceUtils.java index afdd5ada8a..d7b6dbd971 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/NamespaceUtils.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/NamespaceUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -37,6 +37,7 @@ * @author Mark Pollack * @author Dave Syer * @author Gary Russell + * @author Ngoc Nhan * */ public abstract class NamespaceUtils { @@ -249,7 +250,7 @@ public static void parseDeclarationControls(Element element, BeanDefinitionBuild String admins = element.getAttribute("declared-by"); if (StringUtils.hasText(admins)) { String[] adminBeanNames = admins.split(","); - ManagedList adminBeanRefs = new ManagedList(); + ManagedList adminBeanRefs = new ManagedList<>(); for (String adminBeanName : adminBeanNames) { adminBeanRefs.add(new RuntimeBeanReference(adminBeanName.trim())); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.java index fdf057a34b..b4fed509e0 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -36,15 +36,16 @@ * @author Josh Chappelle * @author Gary Russell * @author Leonardo Ferreira + * @author Ngoc Nhan * @since 1.3 */ public abstract class AbstractRoutingConnectionFactory implements ConnectionFactory, RoutingConnectionFactory, InitializingBean, DisposableBean { private final Map targetConnectionFactories = - new ConcurrentHashMap(); + new ConcurrentHashMap<>(); - private final List connectionListeners = new ArrayList(); + private final List connectionListeners = new ArrayList<>(); private ConnectionFactory defaultTargetConnectionFactory; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CompositeChannelListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CompositeChannelListener.java index 81c7dd532f..66399f31e4 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CompositeChannelListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CompositeChannelListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -25,11 +25,12 @@ /** * @author Dave Syer * @author Gary Russell + * @author Ngoc Nhan * */ public class CompositeChannelListener implements ChannelListener { - private List delegates = new ArrayList(); + private List delegates = new ArrayList<>(); public void onCreate(Channel channel, boolean transactional) { for (ChannelListener delegate : this.delegates) { @@ -45,7 +46,7 @@ public void onShutDown(ShutdownSignalException signal) { } public void setDelegates(List delegates) { - this.delegates = new ArrayList(delegates); + this.delegates = new ArrayList<>(delegates); } public void addDelegate(ChannelListener delegate) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CompositeConnectionListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CompositeConnectionListener.java index ce1b01a7b2..0c9900cd77 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CompositeConnectionListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CompositeConnectionListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -27,11 +27,12 @@ * * @author Dave Syer * @author Gary Russell + * @author Ngoc Nhan * */ public class CompositeConnectionListener implements ConnectionListener { - private List delegates = new CopyOnWriteArrayList(); + private List delegates = new CopyOnWriteArrayList<>(); @Override public void onCreate(Connection connection) { @@ -54,7 +55,7 @@ public void onFailed(Exception exception) { } public void setDelegates(List delegates) { - this.delegates = new ArrayList(delegates); + this.delegates = new ArrayList<>(delegates); } public void addDelegate(ConnectionListener delegate) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConsumerChannelRegistry.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConsumerChannelRegistry.java index 0aac6947b3..d1a82f4d87 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConsumerChannelRegistry.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConsumerChannelRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -31,6 +31,7 @@ * tangle with RabbitResourceHolder. * * @author Gary Russell + * @author Ngoc Nhan * @since 1.2 * */ @@ -39,7 +40,7 @@ public final class ConsumerChannelRegistry { private static final Log logger = LogFactory.getLog(ConsumerChannelRegistry.class); // NOSONAR - lower case private static final ThreadLocal consumerChannel // NOSONAR - lower case - = new ThreadLocal(); + = new ThreadLocal<>(); private ConsumerChannelRegistry() { } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java index e84ed71190..c27986322b 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2023 the original author or authors. + * Copyright 2015-2024 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. @@ -52,6 +52,7 @@ * * @author Gary Russell * @author Christian Tzolov + * @author Ngoc Nhan * @since 1.2 */ public class LocalizedQueueConnectionFactory implements ConnectionFactory, RoutingConnectionFactory, DisposableBean, @@ -61,13 +62,13 @@ public class LocalizedQueueConnectionFactory implements ConnectionFactory, Routi private final Lock lock = new ReentrantLock(); - private final Map nodeFactories = new HashMap(); + private final Map nodeFactories = new HashMap<>(); private final ConnectionFactory defaultConnectionFactory; private final String[] adminUris; - private final Map nodeToAddress = new HashMap(); + private final Map nodeToAddress = new HashMap<>(); private final String vhost; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java index c298c118a9..12284d5bca 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -91,6 +91,7 @@ * @author Arnaud Cogoluègnes * @author Artem Bilan * @author Christian Tzolov + * @author Ngoc Nhan * * @since 1.0.1 * @@ -924,7 +925,7 @@ public Collection expire(Listener listener, long cutoffTime) { return Collections.emptyList(); } else { - List expired = new ArrayList(); + List expired = new ArrayList<>(); Iterator> iterator = pendingConfirmsForListener.entrySet().iterator(); while (iterator.hasNext()) { PendingConfirm pendingConfirm = iterator.next().getValue(); @@ -1025,7 +1026,7 @@ private void processMultipleAck(long seq, boolean ack) { */ Map involvedListeners = this.listenerForSeq.headMap(seq + 1); // eliminate duplicates - Set listenersForAcks = new HashSet(involvedListeners.values()); + Set listenersForAcks = new HashSet<>(involvedListeners.values()); for (Listener involvedListener : listenersForAcks) { // find all unack'd confirms for this listener and handle them SortedMap confirmsMap = this.pendingConfirms.get(involvedListener); @@ -1047,7 +1048,7 @@ private void processMultipleAck(long seq, boolean ack) { } } } - List seqs = new ArrayList(involvedListeners.keySet()); + List seqs = new ArrayList<>(involvedListeners.keySet()); for (Long key : seqs) { this.listenerForSeq.remove(key); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitResourceHolder.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitResourceHolder.java index 8cea93e04e..80fd02e75a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitResourceHolder.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitResourceHolder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -45,6 +45,7 @@ * @author Mark Fisher * @author Dave Syer * @author Gary Russell + * @author Ngoc Nhan * * @see org.springframework.amqp.rabbit.transaction.RabbitTransactionManager * @see org.springframework.amqp.rabbit.core.RabbitTemplate @@ -120,7 +121,7 @@ public final void addChannel(Channel channel, @Nullable Connection connection) { if (connection != null) { List channelsForConnection = this.channelsPerConnection.get(connection); if (channelsForConnection == null) { - channelsForConnection = new LinkedList(); + channelsForConnection = new LinkedList<>(); this.channelsPerConnection.put(connection, channelsForConnection); } channelsForConnection.add(channel); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleResourceHolder.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleResourceHolder.java index bacdf4a61f..f0861a0497 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleResourceHolder.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleResourceHolder.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2024 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. @@ -45,6 +45,7 @@ * * @author Artem Bilan * @author Gary Russell + * @author Ngoc Nhan * @since 1.3 */ public final class SimpleResourceHolder { @@ -56,10 +57,10 @@ public final class SimpleResourceHolder { private static final Log LOGGER = LogFactory.getLog(SimpleResourceHolder.class); private static final ThreadLocal> RESOURCES = - new NamedThreadLocal>("Simple resources"); + new NamedThreadLocal<>("Simple resources"); private static final ThreadLocal>> STACK = - new NamedThreadLocal>>("Simple resources"); + new NamedThreadLocal<>("Simple resources"); /** * Return all resources that are bound to the current thread. @@ -126,7 +127,7 @@ public static void bind(Object key, Object value) { Map map = RESOURCES.get(); // set ThreadLocal Map if none found if (map == null) { - map = new HashMap(); + map = new HashMap<>(); RESOURCES.set(map); } Object oldValue = map.put(key, value); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java index a4031e88dc..7ea93712fe 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2024 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. @@ -46,6 +46,7 @@ * @author Stephane Nicoll * @author Gary Russell * @author Artem Bilan + * @author Ngoc Nhan * * @since 1.4 * @@ -298,7 +299,7 @@ public void setTaskExecutor(TaskExecutor taskExecutor) { * @return true if batch. */ public boolean isBatchListener() { - return this.batchListener == null ? false : this.batchListener; + return this.batchListener != null && this.batchListener; } @Override diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ConditionalRejectingErrorHandler.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ConditionalRejectingErrorHandler.java index acd7cbedb8..33204d1b64 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ConditionalRejectingErrorHandler.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ConditionalRejectingErrorHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2024 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. @@ -50,6 +50,7 @@ * {@link AmqpRejectAndDontRequeueException}. * * @author Gary Russell + * @author Ngoc Nhan * @since 1.3.2 * */ @@ -136,7 +137,7 @@ public void handleError(Throwable t) { Message failed = lefe.getFailedMessage(); if (failed != null) { List> xDeath = failed.getMessageProperties().getXDeathHeader(); - if (xDeath != null && xDeath.size() > 0) { + if (xDeath != null && !xDeath.isEmpty()) { this.logger.error("x-death header detected on a message with a fatal exception; " + "perhaps requeued from a DLQ? - discarding: " + failed); handleDiscarded(failed); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MultiMethodRabbitListenerEndpoint.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MultiMethodRabbitListenerEndpoint.java index 2f5ebbd865..1833eee771 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MultiMethodRabbitListenerEndpoint.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MultiMethodRabbitListenerEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2022 the original author or authors. + * Copyright 2015-2024 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. @@ -29,6 +29,7 @@ /** * @author Gary Russell + * @author Ngoc Nhan * @since 1.5 * */ @@ -64,7 +65,7 @@ public void setValidator(Validator validator) { @Override protected HandlerAdapter configureListenerAdapter(MessagingMessageListenerAdapter messageListener) { - List invocableHandlerMethods = new ArrayList(); + List invocableHandlerMethods = new ArrayList<>(); InvocableHandlerMethod defaultHandler = null; for (Method method : this.methods) { InvocableHandlerMethod handler = getMessageHandlerMethodFactory() diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ActiveObjectCounter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ActiveObjectCounter.java index 30abbced57..e37a5653be 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ActiveObjectCounter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ActiveObjectCounter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -29,11 +29,12 @@ * * @author Dave Syer * @author Artem Bilan + * @author Ngoc Nhan * */ public class ActiveObjectCounter { - private final ConcurrentMap locks = new ConcurrentHashMap(); + private final ConcurrentMap locks = new ConcurrentHashMap<>(); private volatile boolean active = true; @@ -56,7 +57,7 @@ public boolean await(long timeout, TimeUnit timeUnit) throws InterruptedExceptio if (this.locks.isEmpty()) { return true; } - Collection objects = new HashSet(this.locks.keySet()); + Collection objects = new HashSet<>(this.locks.keySet()); for (T object : objects) { CountDownLatch lock = this.locks.get(object); if (lock == null) { From 7a303554ea196dee203dcc50b53c1b153a2aaecc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 28 Sep 2024 03:02:36 +0000 Subject: [PATCH 558/737] Bump com.github.spotbugs in the development-dependencies group (#2843) Bumps the development-dependencies group with 1 update: com.github.spotbugs. Updates `com.github.spotbugs` from 6.0.22 to 6.0.23 --- updated-dependencies: - dependency-name: com.github.spotbugs dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 284c3366ec..efb4b10277 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ plugins { id 'io.spring.dependency-management' version '1.1.6' apply false id 'org.antora' version '1.0.0' id 'io.spring.antora.generate-antora-yml' version '0.0.1' - id 'com.github.spotbugs' version '6.0.22' + id 'com.github.spotbugs' version '6.0.23' id 'io.freefair.aggregate-javadoc' version '8.6' } From ba404b26330d0a870322b08a43ea27108fda0e38 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 28 Sep 2024 03:02:44 +0000 Subject: [PATCH 559/737] Bump com.github.luben:zstd-jni from 1.5.6-5 to 1.5.6-6 (#2844) Bumps [com.github.luben:zstd-jni](https://github.com/luben/zstd-jni) from 1.5.6-5 to 1.5.6-6. - [Commits](https://github.com/luben/zstd-jni/compare/v1.5.6-5...v1.5.6-6) --- updated-dependencies: - dependency-name: com.github.luben:zstd-jni dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index efb4b10277..3d946dc637 100644 --- a/build.gradle +++ b/build.gradle @@ -73,7 +73,7 @@ ext { springRetryVersion = '2.0.9' springVersion = '6.2.0-SNAPSHOT' testcontainersVersion = '1.20.1' - zstdJniVersion = '1.5.6-5' + zstdJniVersion = '1.5.6-6' javaProjects = subprojects - project(':spring-amqp-bom') } From ce9c67ca9396a4bcbf96b94ad546cf55e5d4474d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 28 Sep 2024 03:02:52 +0000 Subject: [PATCH 560/737] Bump org.junit:junit-bom from 5.11.0 to 5.11.1 (#2845) Bumps [org.junit:junit-bom](https://github.com/junit-team/junit5) from 5.11.0 to 5.11.1. - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.11.0...r5.11.1) --- updated-dependencies: - dependency-name: org.junit:junit-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 3d946dc637..048fa0ce0d 100644 --- a/build.gradle +++ b/build.gradle @@ -56,7 +56,7 @@ ext { jacksonBomVersion = '2.17.2' jaywayJsonPathVersion = '2.9.0' junit4Version = '4.13.2' - junitJupiterVersion = '5.11.0' + junitJupiterVersion = '5.11.1' kotlinCoroutinesVersion = '1.8.1' log4jVersion = '2.24.0' logbackVersion = '1.5.8' From a597c1574d9b3777160cd11ea3d34eda7c10d87d Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Mon, 30 Sep 2024 20:41:46 +0700 Subject: [PATCH 561/737] Modernize more code for diamond, isEmpty & pattern matching --- .../amqp/core/BindingBuilder.java | 2 +- .../listener/StreamListenerContainer.java | 7 ++-- .../DefaultStreamMessageConverter.java | 3 +- ...itListenerAnnotationBeanPostProcessor.java | 6 ++-- .../rabbit/batch/SimpleBatchingStrategy.java | 6 ++-- .../rabbit/config/HeadersExchangeParser.java | 5 +-- .../config/ListenerContainerFactoryBean.java | 3 +- .../config/ListenerContainerParser.java | 9 +++--- .../amqp/rabbit/config/QueueParser.java | 5 +-- .../rabbit/config/RabbitNamespaceUtils.java | 32 ++++++++----------- .../amqp/rabbit/config/TemplateParser.java | 3 +- .../PublisherCallbackChannelImpl.java | 4 +-- .../connection/SimpleResourceHolder.java | 2 +- .../ThreadChannelConnectionFactory.java | 5 +-- .../rabbit/core/BatchingRabbitTemplate.java | 4 +-- .../AbstractRabbitListenerEndpoint.java | 3 +- .../adapter/MessageListenerAdapter.java | 7 ++-- .../retry/RepublishMessageRecoverer.java | 4 +-- 18 files changed, 56 insertions(+), 54 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/BindingBuilder.java b/spring-amqp/src/main/java/org/springframework/amqp/core/BindingBuilder.java index 956d83d8ec..5eceadd960 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/BindingBuilder.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/BindingBuilder.java @@ -82,7 +82,7 @@ public static final class DestinationConfigurer { } public Binding to(FanoutExchange exchange) { - return new Binding(this.queue, this.name, this.type, exchange.getName(), "", new HashMap()); + return new Binding(this.queue, this.name, this.type, exchange.getName(), "", new HashMap<>()); } public HeadersExchangeMapConfigurer to(HeadersExchange exchange) { diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java index 925565cb29..842ca8f071 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors. + * Copyright 2021-2024 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. @@ -56,6 +56,7 @@ * * @author Gary Russell * @author Christian Tzolov + * @author Ngoc Nhan * @since 2.4 * */ @@ -251,7 +252,7 @@ public void afterPropertiesSet() { public boolean isRunning() { this.lock.lock(); try { - return this.consumers.size() > 0; + return !this.consumers.isEmpty(); } finally { this.lock.unlock(); @@ -262,7 +263,7 @@ public boolean isRunning() { public void start() { this.lock.lock(); try { - if (this.consumers.size() == 0) { + if (this.consumers.isEmpty()) { this.consumerCustomizer.accept(getListenerId(), this.builder); if (this.simpleStream) { this.consumers.add(this.builder.build()); diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverter.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverter.java index dd2d765fd7..614f4a6e79 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverter.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverter.java @@ -41,6 +41,7 @@ * Default {@link StreamMessageConverter}. * * @author Gary Russell + * @author Ngoc Nhan * @since 2.4 * */ @@ -105,7 +106,7 @@ public com.rabbitmq.stream.Message fromMessage(Message message) throws MessageCo .acceptIfNotNull(mProps.getGroupSequence(), propsBuilder::groupSequence) .acceptIfNotNull(mProps.getReplyToGroupId(), propsBuilder::replyToGroupId); ApplicationPropertiesBuilder appPropsBuilder = builder.applicationProperties(); - if (mProps.getHeaders().size() > 0) { + if (!mProps.getHeaders().isEmpty()) { mProps.getHeaders().forEach((key, val) -> { mapProp(key, val, appPropsBuilder); }); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java index 40f01b38bc..2c1ce24447 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java @@ -317,12 +317,12 @@ public Object postProcessAfterInitialization(final Object bean, final String bea private TypeMetadata buildMetadata(Class targetClass) { List classLevelListeners = findListenerAnnotations(targetClass); - final boolean hasClassLevelListeners = classLevelListeners.size() > 0; + final boolean hasClassLevelListeners = !classLevelListeners.isEmpty(); final List methods = new ArrayList<>(); final List multiMethods = new ArrayList<>(); ReflectionUtils.doWithMethods(targetClass, method -> { List listenerAnnotations = findListenerAnnotations(method); - if (listenerAnnotations.size() > 0) { + if (!listenerAnnotations.isEmpty()) { methods.add(new ListenerMethod(method, listenerAnnotations.toArray(new RabbitListener[listenerAnnotations.size()]))); } @@ -880,7 +880,7 @@ private Map resolveArguments(Argument[] arguments) { } } } - return map.size() < 1 ? null : map; + return map.isEmpty() ? null : map; } private void addToMap(Map map, String key, Object value, Class typeClass, String typeName) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/SimpleBatchingStrategy.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/SimpleBatchingStrategy.java index 8958cac06c..a7f80b3d46 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/SimpleBatchingStrategy.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/SimpleBatchingStrategy.java @@ -88,7 +88,7 @@ public MessageBatch addToBatch(String exch, String routKey, Message message) { } int bufferUse = Integer.BYTES + message.getBody().length; MessageBatch batch = null; - if (this.messages.size() > 0 && this.currentSize + bufferUse > this.bufferLimit) { + if (!this.messages.isEmpty() && this.currentSize + bufferUse > this.bufferLimit) { batch = doReleaseBatch(); this.exchange = exch; this.routingKey = routKey; @@ -104,7 +104,7 @@ public MessageBatch addToBatch(String exch, String routKey, Message message) { @Override public Date nextRelease() { - if (this.messages.size() == 0 || this.timeout <= 0) { + if (this.messages.isEmpty() || this.timeout <= 0) { return null; } else if (this.currentSize >= this.bufferLimit) { @@ -128,7 +128,7 @@ public Collection releaseBatches() { } private MessageBatch doReleaseBatch() { - if (this.messages.size() < 1) { + if (this.messages.isEmpty()) { return null; } Message message = assembleMessage(); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/HeadersExchangeParser.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/HeadersExchangeParser.java index fc38d35a90..0cb49dbab2 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/HeadersExchangeParser.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/HeadersExchangeParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -30,6 +30,7 @@ * @author Dave Syer * @author Gary Russell * @author Artem Bilan + * @author Ngoc Nhan * */ public class HeadersExchangeParser extends AbstractExchangeParser { @@ -63,7 +64,7 @@ protected BeanDefinitionBuilder parseBinding(String exchangeName, Element bindin parserContext.getReaderContext() .error("At least one of 'binding-arguments' sub-element or 'key/value' attributes pair have to be declared.", binding); } - ManagedMap map = new ManagedMap(); + ManagedMap map = new ManagedMap<>(); map.put(new TypedStringValue(key), new TypedStringValue(value)); builder.addPropertyValue("arguments", map); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java index 0a070e1eca..c919105ea1 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java @@ -56,6 +56,7 @@ * @author Artem Bilan * @author Johno Crawford * @author Jeonggi Kim + * @author Ngoc Nhan * * @since 2.0 * @@ -542,7 +543,7 @@ protected AbstractMessageListenerContainer createInstance() { // NOSONAR complex .acceptIfNotNull(this.exclusiveConsumerExceptionLogger, container::setExclusiveConsumerExceptionLogger) .acceptIfNotNull(this.micrometerEnabled, container::setMicrometerEnabled) - .acceptIfCondition(this.micrometerTags.size() > 0, this.micrometerTags, + .acceptIfCondition(!this.micrometerTags.isEmpty(), this.micrometerTags, container::setMicrometerTags); if (this.smlcCustomizer != null && this.type.equals(Type.simple)) { this.smlcCustomizer.configure((SimpleMessageListenerContainer) container); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerParser.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerParser.java index 0fb64b643c..fce31f9935 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerParser.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -42,6 +42,7 @@ /** * @author Mark Fisher * @author Gary Russell + * @author Ngoc Nhan * @since 1.0 */ class ListenerContainerParser implements BeanDefinitionParser { @@ -188,7 +189,7 @@ private void parseListener(Element listenerEle, Element containerEle, ParserCont } else { String[] names = StringUtils.commaDelimitedListToStringArray(queues); - List values = new ManagedList(); + List values = new ManagedList<>(); for (int i = 0; i < names.length; i++) { values.add(new RuntimeBeanReference(names[i].trim())); } @@ -196,14 +197,14 @@ private void parseListener(Element listenerEle, Element containerEle, ParserCont } } - ManagedMap args = new ManagedMap(); + ManagedMap args = new ManagedMap<>(); String priority = listenerEle.getAttribute("priority"); if (StringUtils.hasText(priority)) { args.put("x-priority", new TypedStringValue(priority, Integer.class)); } - if (args.size() > 0) { + if (!args.isEmpty()) { containerDef.getPropertyValues().add("consumerArguments", args); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/QueueParser.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/QueueParser.java index 4bce257e1a..38314dad06 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/QueueParser.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/QueueParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -33,6 +33,7 @@ * @author Gary Russell * @author Felipe Gutierrez * @author Artem Bilan + * @author Ngoc Nhan * */ public class QueueParser extends AbstractSingleBeanDefinitionParser { @@ -134,7 +135,7 @@ private void parseArguments(Element element, ParserContext parserContext, BeanDe Map map = parserContext.getDelegate().parseMapElement(argumentsElement, builder.getRawBeanDefinition()); if (StringUtils.hasText(ref)) { - if (map != null && map.size() > 0) { + if (map != null && !map.isEmpty()) { parserContext.getReaderContext() .error("You cannot have both a 'ref' and a nested map", element); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/RabbitNamespaceUtils.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/RabbitNamespaceUtils.java index 69ba403406..c219c7bf7c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/RabbitNamespaceUtils.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/RabbitNamespaceUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -358,28 +358,22 @@ public static BeanDefinition parseContainer(Element containerEle, ParserContext } private static AcknowledgeMode parseAcknowledgeMode(Element ele, ParserContext parserContext) { - AcknowledgeMode acknowledgeMode = null; String acknowledge = ele.getAttribute(ACKNOWLEDGE_ATTRIBUTE); if (StringUtils.hasText(acknowledge)) { - if (ACKNOWLEDGE_AUTO.equals(acknowledge)) { - acknowledgeMode = AcknowledgeMode.AUTO; - } - else if (ACKNOWLEDGE_MANUAL.equals(acknowledge)) { - acknowledgeMode = AcknowledgeMode.MANUAL; - } - else if (ACKNOWLEDGE_NONE.equals(acknowledge)) { - acknowledgeMode = AcknowledgeMode.NONE; - } - else { - parserContext.getReaderContext().error( + return switch (acknowledge) { + case ACKNOWLEDGE_AUTO -> AcknowledgeMode.AUTO; + case ACKNOWLEDGE_MANUAL -> AcknowledgeMode.MANUAL; + case ACKNOWLEDGE_NONE -> AcknowledgeMode.NONE; + default -> { + parserContext.getReaderContext().error( "Invalid listener container 'acknowledge' setting [" + acknowledge - + "]: only \"auto\", \"manual\", and \"none\" supported.", ele); - } - return acknowledgeMode; - } - else { - return null; + + "]: only \"auto\", \"manual\", and \"none\" supported.", ele); + yield null; + } + }; } + + return null; } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/TemplateParser.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/TemplateParser.java index 5259fa0e25..c9c5c3f908 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/TemplateParser.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/TemplateParser.java @@ -34,6 +34,7 @@ * @author Dave Syer * @author Gary Russell * @author Artem Bilan + * @author Ngoc Nhan */ class TemplateParser extends AbstractSingleBeanDefinitionParser { @@ -160,7 +161,7 @@ protected void doParse(Element element, ParserContext parserContext, BeanDefinit BeanDefinition replyContainer = null; Element childElement = null; List childElements = DomUtils.getChildElementsByTagName(element, LISTENER_ELEMENT); - if (childElements.size() > 0) { + if (!childElements.isEmpty()) { childElement = childElements.get(0); } if (childElement != null) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java index 12284d5bca..b8ee5e29dd 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java @@ -904,12 +904,12 @@ public int getPendingConfirmsCount() { @Override public void addListener(Listener listener) { Assert.notNull(listener, "Listener cannot be null"); - if (this.listeners.size() == 0) { + if (this.listeners.isEmpty()) { this.delegate.addConfirmListener(this); this.delegate.addReturnListener(this); } if (this.listeners.putIfAbsent(listener.getUUID(), listener) == null) { - this.pendingConfirms.put(listener, new ConcurrentSkipListMap()); + this.pendingConfirms.put(listener, new ConcurrentSkipListMap<>()); if (this.logger.isDebugEnabled()) { this.logger.debug("Added listener " + listener); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleResourceHolder.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleResourceHolder.java index f0861a0497..10036d3ad2 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleResourceHolder.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleResourceHolder.java @@ -176,7 +176,7 @@ public static Object pop(Object key) { Map> stack = STACK.get(); if (stack != null) { Deque deque = stack.get(key); - if (deque != null && deque.size() > 0) { + if (deque != null && !deque.isEmpty()) { Object previousValue = deque.pop(); if (previousValue != null) { bind(key, previousValue); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java index d1b667532f..b9d371e683 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-2024 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. @@ -49,6 +49,7 @@ * @author Gary Russell * @author Leonardo Ferreira * @author Christian Tzolov + * @author Ngoc Nhan * @since 2.3 * */ @@ -191,7 +192,7 @@ public void destroy() { this.connection.forceClose(); this.connection = null; } - if (this.switchesInProgress.size() > 0 && this.logger.isWarnEnabled()) { + if (!this.switchesInProgress.isEmpty() && this.logger.isWarnEnabled()) { this.logger.warn("Unclaimed context switches from threads:" + this.switchesInProgress.values() .stream() diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplate.java index 8982d80986..18e0b11719 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2024 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. @@ -99,7 +99,7 @@ public void send(String exchange, String routingKey, Message message, } Date next = this.batchingStrategy.nextRelease(); if (next != null) { - this.scheduledTask = this.scheduler.schedule((Runnable) () -> releaseBatches(), next.toInstant()); + this.scheduledTask = this.scheduler.schedule(this::releaseBatches, next.toInstant()); } } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java index 7ea93712fe..de0a38f538 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java @@ -397,8 +397,7 @@ public void setupListenerContainer(MessageListenerContainer listenerContainer) { throw new IllegalStateException("Queues or queue names must be provided but not both for " + this); } if (queuesEmpty) { - Collection names = qNames; - container.setQueueNames(names.toArray(new String[0])); + container.setQueueNames(qNames.toArray(new String[0])); } else { Collection instances = getQueues(); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapter.java index c9c90a1871..65667d8453 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -118,6 +118,7 @@ * @author Gary Russell * @author Greg Turnquist * @author Cai Kun + * @author Ngoc Nhan * * @see #setDelegate * @see #setDefaultListenerMethod @@ -129,7 +130,7 @@ */ public class MessageListenerAdapter extends AbstractAdaptableMessageListener { - private final Map queueOrTagToMethodName = new HashMap(); + private final Map queueOrTagToMethodName = new HashMap<>(); /** * Out-of-the-box value for the default listener method: "handleMessage". @@ -314,7 +315,7 @@ else if (delegateListener instanceof MessageListener messageListener) { * @see #setQueueOrTagToMethodName */ protected String getListenerMethodName(Message originalMessage, Object extractedMessage) { - if (this.queueOrTagToMethodName.size() > 0) { + if (!this.queueOrTagToMethodName.isEmpty()) { MessageProperties props = originalMessage.getMessageProperties(); String methodName = this.queueOrTagToMethodName.get(props.getConsumerQueue()); if (methodName == null) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecoverer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecoverer.java index ae84767aa3..61bb2122c7 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecoverer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecoverer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2024 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. @@ -91,7 +91,7 @@ public class RepublishMessageRecoverer implements MessageRecoverer { * @param errorTemplate the template. */ public RepublishMessageRecoverer(AmqpTemplate errorTemplate) { - this(errorTemplate, (String) null, (String) null); + this(errorTemplate, null, (String) null); } /** From cbc2eb386b8bd185793f408e9f9c18370dd9de18 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 1 Oct 2024 17:32:36 -0400 Subject: [PATCH 562/737] Upgrade to Jackson 2.18 * Fix `ObservationTests` for the latest changes with default tags. Related to: https://github.com/spring-projects/spring-amqp/issues/2814 --- build.gradle | 2 +- .../rabbit/support/micrometer/ObservationTests.java | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index 048fa0ce0d..0f5fd35186 100644 --- a/build.gradle +++ b/build.gradle @@ -53,7 +53,7 @@ ext { commonsPoolVersion = '2.12.0' hamcrestVersion = '2.2' hibernateValidationVersion = '8.0.1.Final' - jacksonBomVersion = '2.17.2' + jacksonBomVersion = '2.18.0' jaywayJsonPathVersion = '2.9.0' junit4Version = '4.13.2' junitJupiterVersion = '5.11.1' diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java index 4c0b6fa056..fe1b38995a 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java @@ -67,6 +67,8 @@ /** * @author Gary Russell * @author Ngoc Nhan + * @author Artem Bilan + * * @since 3.0 * */ @@ -91,17 +93,17 @@ void endToEnd(@Autowired Listener listener, @Autowired RabbitTemplate template, SimpleSpan span = spans.poll(); assertThat(span.getTags()).containsEntry("spring.rabbit.template.name", "template"); assertThat(span.getName()).isEqualTo("/observation.testQ1 send"); - await().until(() -> spans.peekFirst().getTags().size() == 3); + await().until(() -> spans.peekFirst().getTags().size() == 5); span = spans.poll(); assertThat(span.getTags()) .containsAllEntriesOf( Map.of("spring.rabbit.listener.id", "obs1", "foo", "some foo value", "bar", "some bar value")); assertThat(span.getName()).isEqualTo("observation.testQ1 receive"); - await().until(() -> spans.peekFirst().getTags().size() == 1); + await().until(() -> spans.peekFirst().getTags().size() == 3); span = spans.poll(); assertThat(span.getTags()).containsEntry("spring.rabbit.template.name", "template"); assertThat(span.getName()).isEqualTo("/observation.testQ2 send"); - await().until(() -> spans.peekFirst().getTags().size() == 3); + await().until(() -> spans.peekFirst().getTags().size() == 5); span = spans.poll(); assertThat(span.getTags()) .containsAllEntriesOf( @@ -141,7 +143,7 @@ public KeyValues getLowCardinalityKeyValues(RabbitMessageReceiverContext context assertThat(span.getTags()).containsEntry("messaging.destination.name", ""); assertThat(span.getTags()).containsEntry("messaging.rabbitmq.destination.routing_key", "observation.testQ1"); assertThat(span.getName()).isEqualTo("/observation.testQ1 send"); - await().until(() -> spans.peekFirst().getTags().size() == 4); + await().until(() -> spans.peekFirst().getTags().size() == 6); span = spans.poll(); assertThat(span.getTags()) .containsAllEntriesOf(Map.of("spring.rabbit.listener.id", "obs1", "foo", "some foo value", "bar", @@ -154,7 +156,7 @@ public KeyValues getLowCardinalityKeyValues(RabbitMessageReceiverContext context assertThat(span.getTags()).containsEntry("messaging.destination.name", ""); assertThat(span.getTags()).containsEntry("messaging.rabbitmq.destination.routing_key", "observation.testQ2"); assertThat(span.getName()).isEqualTo("/observation.testQ2 send"); - await().until(() -> spans.peekFirst().getTags().size() == 3); + await().until(() -> spans.peekFirst().getTags().size() == 5); span = spans.poll(); assertThat(span.getTags()) .containsAllEntriesOf( From 47868257e03b1c09e8cb5d08506655a5989e2363 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 2 Oct 2024 12:16:10 -0400 Subject: [PATCH 563/737] Attempt to use `services:rabbitmq` for `ci-snapshot.yml` Since we cannot propagate `services` down to the reusable workflow, we don't have choice, but build respective job ourselves --- .github/workflows/ci-snapshot.yml | 39 ++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-snapshot.yml b/.github/workflows/ci-snapshot.yml index a21192cd2c..d7efae0383 100644 --- a/.github/workflows/ci-snapshot.yml +++ b/.github/workflows/ci-snapshot.yml @@ -17,10 +17,37 @@ concurrency: jobs: build-snapshot: - uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-snapshot.yml@main - with: - gradleTasks: ${{ github.event_name == 'schedule' && '--rerun-tasks' || '' }} - secrets: + runs-on: ubuntu-latest + name: CI Build SNAPSHOT for ${{ github.ref_name }} + env: DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} - ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} \ No newline at end of file + + services: + rabbitmq: + image: rabbitmq:3.13.7-management + options: --name rabbitmq + ports: + - 5672:5672 + - 15672:15672 + - 5552:5552 + + steps: + - run: docker exec rabbitmq rabbitmq-plugins enable rabbitmq_stream + - uses: actions/checkout@v4 + with: + show-progress: false + + - name: Checkout Common Repo + uses: actions/checkout@v4 + with: + repository: spring-io/spring-github-workflows + path: spring-github-workflows + show-progress: false + + - name: Build and Publish + timeout-minutes: 30 + uses: ./spring-github-workflows/.github/actions/spring-artifactory-gradle-build + with: + gradleTasks: ${{ github.event_name == 'schedule' && '--rerun-tasks' || '' }} + artifactoryUsername: ${{ secrets.ARTIFACTORY_USERNAME }} + artifactoryPassword: ${{ secrets.ARTIFACTORY_PASSWORD }} From e8b4618397d89b3f54f056a1d678df15c6ee53b6 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 2 Oct 2024 12:21:26 -0400 Subject: [PATCH 564/737] Test `ci-snapshot.yml` without Gradle cache --- .github/workflows/ci-snapshot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-snapshot.yml b/.github/workflows/ci-snapshot.yml index d7efae0383..ba43d17b32 100644 --- a/.github/workflows/ci-snapshot.yml +++ b/.github/workflows/ci-snapshot.yml @@ -48,6 +48,6 @@ jobs: timeout-minutes: 30 uses: ./spring-github-workflows/.github/actions/spring-artifactory-gradle-build with: - gradleTasks: ${{ github.event_name == 'schedule' && '--rerun-tasks' || '' }} + gradleTasks: ${{ github.event_name == 'schedule' && '--rerun-tasks' || '--rerun-tasks' }} artifactoryUsername: ${{ secrets.ARTIFACTORY_USERNAME }} artifactoryPassword: ${{ secrets.ARTIFACTORY_PASSWORD }} From 0223c1cffa42345ac53c6ae5ba5d4427d596630a Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 2 Oct 2024 12:31:47 -0400 Subject: [PATCH 565/737] The `services:rabbitmq` does not work well somehow --- .github/workflows/ci-snapshot.yml | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci-snapshot.yml b/.github/workflows/ci-snapshot.yml index ba43d17b32..116110e66d 100644 --- a/.github/workflows/ci-snapshot.yml +++ b/.github/workflows/ci-snapshot.yml @@ -22,17 +22,13 @@ jobs: env: DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} - services: - rabbitmq: - image: rabbitmq:3.13.7-management - options: --name rabbitmq - ports: - - 5672:5672 - - 15672:15672 - - 5552:5552 - steps: - - run: docker exec rabbitmq rabbitmq-plugins enable rabbitmq_stream + - name: Start RabbitMQ + uses: namoshek/rabbitmq-github-action@v1 + with: + ports: '5672:5672 15672:15672 5552:5552' + plugins: rabbitmq_stream,rabbitmq_management + - uses: actions/checkout@v4 with: show-progress: false From 966b29f1fe846d20f6f2e22b6d8e131f3b0e7b5b Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 2 Oct 2024 12:41:12 -0400 Subject: [PATCH 566/737] Add more plugins to RabbitMQ action for `ci-snapshot.yml` --- .github/workflows/ci-snapshot.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-snapshot.yml b/.github/workflows/ci-snapshot.yml index 116110e66d..37070bd9c2 100644 --- a/.github/workflows/ci-snapshot.yml +++ b/.github/workflows/ci-snapshot.yml @@ -27,7 +27,7 @@ jobs: uses: namoshek/rabbitmq-github-action@v1 with: ports: '5672:5672 15672:15672 5552:5552' - plugins: rabbitmq_stream,rabbitmq_management + plugins: rabbitmq_stream,rabbitmq_management,rabbitmq_delayed_message_exchange,rabbitmq_consistent_hash_exchange - uses: actions/checkout@v4 with: @@ -44,6 +44,6 @@ jobs: timeout-minutes: 30 uses: ./spring-github-workflows/.github/actions/spring-artifactory-gradle-build with: - gradleTasks: ${{ github.event_name == 'schedule' && '--rerun-tasks' || '--rerun-tasks' }} + gradleTasks: ${{ github.event_name == 'schedule' && '--rerun-tasks' || '' }} artifactoryUsername: ${{ secrets.ARTIFACTORY_USERNAME }} artifactoryPassword: ${{ secrets.ARTIFACTORY_PASSWORD }} From 949bf52a99a3ec041c51eb1ecc6a53540d9ef7b4 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 3 Oct 2024 14:10:16 -0400 Subject: [PATCH 567/737] Fix `RabbitUtils` for actual reason when cannot declare exchange **Auto-cherry-pick to `3.1.x`** --- .../springframework/amqp/rabbit/connection/RabbitUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java index 4a958f6529..1d0c1609f8 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java @@ -354,8 +354,8 @@ public static boolean isExchangeDeclarationFailure(Exception e) { } else { Method shutdownReason = sig.getReason(); - return shutdownReason instanceof AMQP.Connection.Close closeReason - && AMQP.COMMAND_INVALID == closeReason.getReplyCode() + return shutdownReason instanceof AMQP.Channel.Close closeReason + && AMQP.PRECONDITION_FAILED == closeReason.getReplyCode() && closeReason.getClassId() == EXCHANGE_CLASS_ID_40 && closeReason.getMethodId() == DECLARE_METHOD_ID_10; } From 82bed3f24efaaa4ecce7765abe232d7bb9811e07 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 4 Oct 2024 10:08:25 -0400 Subject: [PATCH 568/737] Fix timing in the `AsyncRabbitTemplateTests` --- .../springframework/amqp/rabbit/AsyncRabbitTemplateTests.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java index 1c72c9db77..e054a7abab 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java @@ -473,7 +473,8 @@ private Message checkMessageResult(CompletableFuture future, String exp }); assertThat(cdl.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(new String(resultRef.get().getBody())).isEqualTo(expected); - assertThat(TestUtils.getPropertyValue(future, "timeoutTask", Future.class).isCancelled()).isTrue(); + await().untilAsserted(() -> + assertThat(TestUtils.getPropertyValue(future, "timeoutTask", Future.class).isCancelled()).isTrue()); return resultRef.get(); } From b76b6195f58ab2d8b3ddc84493c2a140d0c64055 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 4 Oct 2024 12:12:35 -0400 Subject: [PATCH 569/737] Fix race condition in the `EnableRabbitKotlinTests.kt` --- .../amqp/rabbit/annotation/EnableRabbitKotlinTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt b/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt index 87fe6dba34..4b94a657e6 100644 --- a/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt +++ b/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt @@ -105,8 +105,8 @@ class EnableRabbitKotlinTests { @RabbitListener(id = "batch", queues = ["kotlinBatchQueue"], containerFactory = "batchRabbitListenerContainerFactory") suspend fun receiveBatch(messages: List) { - batchReceived.countDown() batch = messages + batchReceived.countDown() } @Bean From cdf36cd38c5a1a7f2e7622be53acb2b0b6eba6ee Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 4 Oct 2024 12:26:39 -0400 Subject: [PATCH 570/737] Fix race condition in the `MessageListenerContainerMultipleQueueIntegrationTests` --- ...ontainerMultipleQueueIntegrationTests.java | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerMultipleQueueIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerMultipleQueueIntegrationTests.java index 155eeaa885..9930f42929 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerMultipleQueueIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerMultipleQueueIntegrationTests.java @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -36,15 +34,18 @@ import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter; import org.springframework.amqp.support.converter.SimpleMessageConverter; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Mark Fisher * @author Gunnar Hillert * @author Gary Russell + * @author Artem Bilan */ -@RabbitAvailable(queues = { MessageListenerContainerMultipleQueueIntegrationTests.TEST_QUEUE_1, - MessageListenerContainerMultipleQueueIntegrationTests.TEST_QUEUE_2 }) -@LogLevels(level = "INFO", classes = { RabbitTemplate.class, - SimpleMessageListenerContainer.class, BlockingQueueConsumer.class }) +@RabbitAvailable(queues = {MessageListenerContainerMultipleQueueIntegrationTests.TEST_QUEUE_1, + MessageListenerContainerMultipleQueueIntegrationTests.TEST_QUEUE_2}) +@LogLevels(level = "INFO", classes = {RabbitTemplate.class, + SimpleMessageListenerContainer.class, BlockingQueueConsumer.class}) public class MessageListenerContainerMultipleQueueIntegrationTests { public static final String TEST_QUEUE_1 = "test.queue.1.MessageListenerContainerMultipleQueueIntegrationTests"; @@ -77,7 +78,6 @@ public void testMultipleQueueNamesWithConcurrentConsumers() { doTest(3, container -> container.setQueueNames(queue1.getName(), queue2.getName())); } - private void doTest(int concurrentConsumers, ContainerConfigurer configurer) { int messageCount = 10; RabbitTemplate template = new RabbitTemplate(); @@ -119,17 +119,15 @@ private void doTest(int concurrentConsumers, ContainerConfigurer configurer) { container.shutdown(); assertThat(container.getActiveConsumerCount()).isEqualTo(0); } - assertThat(template.receiveAndConvert(queue1.getName())).isNull(); - assertThat(template.receiveAndConvert(queue2.getName())).isNull(); - connectionFactory.destroy(); } @FunctionalInterface private interface ContainerConfigurer { + void configure(SimpleMessageListenerContainer container); - } + } @SuppressWarnings("unused") private static class PojoListener { @@ -150,6 +148,7 @@ public void handleMessage(int value) throws Exception { public int getCount() { return count.get(); } + } } From 39d1325feb88d6f7aca954838335887c4c9a70cf Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 4 Oct 2024 12:34:08 -0400 Subject: [PATCH 571/737] Fix Checkstyle violation for imports order --- ...MessageListenerContainerMultipleQueueIntegrationTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerMultipleQueueIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerMultipleQueueIntegrationTests.java index 9930f42929..d2fe778b84 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerMultipleQueueIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerMultipleQueueIntegrationTests.java @@ -16,6 +16,8 @@ package org.springframework.amqp.rabbit.listener; +import static org.assertj.core.api.Assertions.assertThat; + import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -34,8 +36,6 @@ import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter; import org.springframework.amqp.support.converter.SimpleMessageConverter; -import static org.assertj.core.api.Assertions.assertThat; - /** * @author Mark Fisher * @author Gunnar Hillert From 5b24690d44624f24a954298ea1c54a00b6add9db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 5 Oct 2024 02:13:50 +0000 Subject: [PATCH 572/737] Bump the development-dependencies group with 2 updates (#2847) Bumps the development-dependencies group with 2 updates: com.github.spotbugs and [io.spring.develocity.conventions](https://github.com/spring-io/develocity-conventions). Updates `com.github.spotbugs` from 6.0.23 to 6.0.24 Updates `io.spring.develocity.conventions` from 0.0.21 to 0.0.22 - [Release notes](https://github.com/spring-io/develocity-conventions/releases) - [Commits](https://github.com/spring-io/develocity-conventions/compare/v0.0.21...v0.0.22) --- updated-dependencies: - dependency-name: com.github.spotbugs dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies - dependency-name: io.spring.develocity.conventions dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- settings.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 0f5fd35186..3ced3a412f 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ plugins { id 'io.spring.dependency-management' version '1.1.6' apply false id 'org.antora' version '1.0.0' id 'io.spring.antora.generate-antora-yml' version '0.0.1' - id 'com.github.spotbugs' version '6.0.23' + id 'com.github.spotbugs' version '6.0.24' id 'io.freefair.aggregate-javadoc' version '8.6' } diff --git a/settings.gradle b/settings.gradle index 87c12dd0fa..083f33a8a1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,7 +6,7 @@ pluginManagement { } plugins { - id 'io.spring.develocity.conventions' version '0.0.21' + id 'io.spring.develocity.conventions' version '0.0.22' } rootProject.name = 'spring-amqp-dist' From 5075525bcfa6cb4dd9ca30a6823b7228c88b06a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 5 Oct 2024 02:14:03 +0000 Subject: [PATCH 573/737] Bump log4jVersion from 2.24.0 to 2.24.1 (#2849) Bumps `log4jVersion` from 2.24.0 to 2.24.1. Updates `org.apache.logging.log4j:log4j-bom` from 2.24.0 to 2.24.1 - [Release notes](https://github.com/apache/logging-log4j2/releases) - [Changelog](https://github.com/apache/logging-log4j2/blob/2.x/RELEASE-NOTES.adoc) - [Commits](https://github.com/apache/logging-log4j2/compare/rel/2.24.0...rel/2.24.1) Updates `org.apache.logging.log4j:log4j-slf4j-impl` from 2.24.0 to 2.24.1 --- updated-dependencies: - dependency-name: org.apache.logging.log4j:log4j-bom dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.logging.log4j:log4j-slf4j-impl dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 3ced3a412f..efe1ea113a 100644 --- a/build.gradle +++ b/build.gradle @@ -58,7 +58,7 @@ ext { junit4Version = '4.13.2' junitJupiterVersion = '5.11.1' kotlinCoroutinesVersion = '1.8.1' - log4jVersion = '2.24.0' + log4jVersion = '2.24.1' logbackVersion = '1.5.8' lz4Version = '1.8.0' micrometerDocsVersion = '1.0.4' From 57689702c71b0f88d0e05958d37db61184bd96ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 5 Oct 2024 02:14:16 +0000 Subject: [PATCH 574/737] Bump org.testcontainers:testcontainers-bom from 1.20.1 to 1.20.2 (#2848) Bumps [org.testcontainers:testcontainers-bom](https://github.com/testcontainers/testcontainers-java) from 1.20.1 to 1.20.2. - [Release notes](https://github.com/testcontainers/testcontainers-java/releases) - [Changelog](https://github.com/testcontainers/testcontainers-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/testcontainers/testcontainers-java/compare/1.20.1...1.20.2) --- updated-dependencies: - dependency-name: org.testcontainers:testcontainers-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index efe1ea113a..2a68d442cc 100644 --- a/build.gradle +++ b/build.gradle @@ -72,7 +72,7 @@ ext { springDataVersion = '2024.0.4' springRetryVersion = '2.0.9' springVersion = '6.2.0-SNAPSHOT' - testcontainersVersion = '1.20.1' + testcontainersVersion = '1.20.2' zstdJniVersion = '1.5.6-6' javaProjects = subprojects - project(':spring-amqp-bom') From 1b6ed72b0af1f15c3cab853a0dfa6e8735004d9c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 5 Oct 2024 02:14:30 +0000 Subject: [PATCH 575/737] Bump org.junit:junit-bom from 5.11.1 to 5.11.2 (#2850) Bumps [org.junit:junit-bom](https://github.com/junit-team/junit5) from 5.11.1 to 5.11.2. - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.11.1...r5.11.2) --- updated-dependencies: - dependency-name: org.junit:junit-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2a68d442cc..6f983cea93 100644 --- a/build.gradle +++ b/build.gradle @@ -56,7 +56,7 @@ ext { jacksonBomVersion = '2.18.0' jaywayJsonPathVersion = '2.9.0' junit4Version = '4.13.2' - junitJupiterVersion = '5.11.1' + junitJupiterVersion = '5.11.2' kotlinCoroutinesVersion = '1.8.1' log4jVersion = '2.24.1' logbackVersion = '1.5.8' From 310a76d919d4aa8749ec6c1d1657f5eb8c243186 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 7 Oct 2024 17:08:29 -0400 Subject: [PATCH 576/737] GH-2688: Introduce a `MessageProperties.RETRY_COUNT` header Fixes: https://github.com/spring-projects/spring-amqp/issues/2688 Since RabbitMQ 4.0 ignored `x-*` headers sent from the client, there is no way to rely on the `x-death.count` increment be done on the broker side when manual re-publishing approach is done from the application side. * Add a `retry-count` header for manual retries. * Map the `x-death.count` into this header on the client for convenience * Document how this `retry-count` header can be used --- .../amqp/core/MessageProperties.java | 36 ++++++++++++++++++ .../amqp/support/AmqpHeaders.java | 8 +++- .../amqp/support/SimpleAmqpHeaderMapper.java | 7 ++-- .../DefaultMessagePropertiesConverter.java | 21 ++++++++++- ...ering-from-errors-and-broker-failures.adoc | 37 +++++++++++++++++++ .../antora/modules/ROOT/pages/whats-new.adoc | 7 +++- 6 files changed, 110 insertions(+), 6 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java index efb2af4b61..75a402a4d2 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java @@ -63,6 +63,13 @@ public class MessageProperties implements Serializable { public static final String X_DELAY = "x-delay"; + /** + * The custom header to represent a number of retries a message is republished. + * In case of server-side DLX, this header contains the value of {@code x-death.count} property. + * When republish is done manually, this header has to be incremented by the application. + */ + public static final String RETRY_COUNT = "retry-count"; + public static final String DEFAULT_CONTENT_TYPE = CONTENT_TYPE_BYTES; public static final MessageDeliveryMode DEFAULT_DELIVERY_MODE = MessageDeliveryMode.PERSISTENT; @@ -131,6 +138,8 @@ public class MessageProperties implements Serializable { private MessageDeliveryMode receivedDeliveryMode; + private long retryCount; + private boolean finalRetryForMessageWithNoId; private long publishSequenceNumber; @@ -468,6 +477,33 @@ public void setDelayLong(Long delay) { this.headers.put(X_DELAY, delay); } + /** + * The number of retries for this message over broker. + * @return the retry count + * @since 3.2 + */ + public long getRetryCount() { + return this.retryCount; + } + + /** + * Set a number of retries for this message over broker. + * @param retryCount the retry count. + * @since 3.2 + * @see #incrementRetryCount() + */ + public void setRetryCount(long retryCount) { + this.retryCount = retryCount; + } + + /** + * Increment a retry count for this message when it is re-published back to the broker. + * @since 3.2 + */ + public void incrementRetryCount() { + this.retryCount++; + } + public boolean isFinalRetryForMessageWithNoId() { return this.finalRetryForMessageWithNoId; } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/AmqpHeaders.java b/spring-amqp/src/main/java/org/springframework/amqp/support/AmqpHeaders.java index a4f64b6475..5478b48573 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/AmqpHeaders.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/AmqpHeaders.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -138,4 +138,10 @@ public abstract class AmqpHeaders { */ public static final String BATCH_SIZE = PREFIX + "batchSize"; + /** + * The number of retries for the message over server republishing. + * @since 3.2 + */ + public static final String RETRY_COUNT = PREFIX + "retryCount"; + } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/SimpleAmqpHeaderMapper.java b/spring-amqp/src/main/java/org/springframework/amqp/support/SimpleAmqpHeaderMapper.java index 2885f91f46..3f3f5bffbb 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/SimpleAmqpHeaderMapper.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/SimpleAmqpHeaderMapper.java @@ -97,6 +97,8 @@ public void fromHeaders(MessageHeaders headers, MessageProperties amqpMessagePro amqpMessageProperties::setTimestamp) .acceptIfNotNull(getHeaderIfAvailable(headers, AmqpHeaders.TYPE, String.class), amqpMessageProperties::setType) + .acceptIfNotNull(getHeaderIfAvailable(headers, AmqpHeaders.RETRY_COUNT, Long.class), + amqpMessageProperties::setRetryCount) .acceptIfHasText(getHeaderIfAvailable(headers, AmqpHeaders.USER_ID, String.class), amqpMessageProperties::setUserId); @@ -166,11 +168,10 @@ public MessageHeaders toHeaders(MessageProperties amqpMessageProperties) { .acceptIfHasText(AmqpHeaders.CONSUMER_TAG, amqpMessageProperties.getConsumerTag(), putString) .acceptIfHasText(AmqpHeaders.CONSUMER_QUEUE, amqpMessageProperties.getConsumerQueue(), putString); headers.put(AmqpHeaders.LAST_IN_BATCH, amqpMessageProperties.isLastInBatch()); + headers.put(AmqpHeaders.RETRY_COUNT, amqpMessageProperties.getRetryCount()); // Map custom headers - for (Map.Entry entry : amqpMessageProperties.getHeaders().entrySet()) { - headers.put(entry.getKey(), entry.getValue()); - } + headers.putAll(amqpMessageProperties.getHeaders()); } catch (Exception e) { if (logger.isWarnEnabled()) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java index 489107b7aa..24632abe0c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java @@ -100,6 +100,12 @@ public MessageProperties toMessageProperties(BasicProperties source, @Nullable E target.setHeader(key, receivedDelayLongValue); } } + else if (MessageProperties.RETRY_COUNT.equals(key)) { + Object value = entry.getValue(); + if (value instanceof Number numberValue) { + target.setRetryCount(numberValue.longValue()); + } + } else { target.setHeader(key, convertLongStringIfNecessary(entry.getValue(), charset)); } @@ -134,13 +140,26 @@ public MessageProperties toMessageProperties(BasicProperties source, @Nullable E target.setRedelivered(envelope.isRedeliver()); target.setDeliveryTag(envelope.getDeliveryTag()); } + + if (target.getRetryCount() == 0) { + List> xDeathHeader = target.getXDeathHeader(); + if (!CollectionUtils.isEmpty(xDeathHeader)) { + target.setRetryCount((long) xDeathHeader.get(0).get("count")); + } + } + return target; } @Override public BasicProperties fromMessageProperties(final MessageProperties source, final String charset) { BasicProperties.Builder target = new BasicProperties.Builder(); - target.headers(this.convertHeadersIfNecessary(source.getHeaders())) + Map headers = convertHeadersIfNecessary(source.getHeaders()); + long retryCount = source.getRetryCount(); + if (retryCount > 0) { + headers.put(MessageProperties.RETRY_COUNT, retryCount); + } + target.headers(headers) .timestamp(source.getTimestamp()) .messageId(source.getMessageId()) .userId(source.getUserId()) diff --git a/src/reference/antora/modules/ROOT/pages/amqp/resilience-recovering-from-errors-and-broker-failures.adoc b/src/reference/antora/modules/ROOT/pages/amqp/resilience-recovering-from-errors-and-broker-failures.adoc index f9c89b3e77..fb35a9723f 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/resilience-recovering-from-errors-and-broker-failures.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/resilience-recovering-from-errors-and-broker-failures.adoc @@ -210,3 +210,40 @@ When `true`, it travers exception causes until it finds a match or there is no c To use this classifier for retry, you can use a `SimpleRetryPolicy` created with the constructor that takes the max attempts, the `Map` of `Exception` instances, and the boolean (`traverseCauses`) and inject this policy into the `RetryTemplate`. +[[retry-over-broker]] +== Retry Over Broker + +The message dead-lettered from the queue can be republished back to this queue after re-routing from a DLX. +This retry behaviour is controlled on the broker side via an `x-death` header. +More information about this approach in the official https://www.rabbitmq.com/docs/dlx[RabbitMQ documentation]. + +The other approach is to re-publish failed message back to the original exchange manually from the application. +Starting with version `4.0`, the RabbitMQ broker does not consider `x-death` header sent from the client. +Essentially, any `x-*` headers are ignored from the client. + +To mitigate this new behavior of the RabbitMQ broker, Spring AMQP has introduced a `retry_count` header starting with version 3.2. +When this header is absent and a server side DLX is in action, the `x-death.count` property is mapped to this header. +When the failed message is re-published manually for retries, the `retry_count` header value has to be incremented manually. +See `MessageProperties.incrementRetryCount()` JavaDocs for more information. + +The following example summarise an algorithm for manual retry over the broker: + +[source,java] +---- +@RabbitListener(queueNames = "some_queue") +public void rePublish(Message message) { + try { + // Process message + } + catch (Exception ex) { + Long retryCount = message.getMessageProperties().getRetryCount(); + if (retryCount < 3) { + message.getMessageProperties().incrementRetryCount(); + this.rabbitTemplate.send("", "some_queue", message); + } + else { + throw new ImmediateAcknowledgeAmqpException("Failed after 4 attempts"); + } + } +} +---- diff --git a/src/reference/antora/modules/ROOT/pages/whats-new.adoc b/src/reference/antora/modules/ROOT/pages/whats-new.adoc index 3258c28f44..0e643ec6ca 100644 --- a/src/reference/antora/modules/ROOT/pages/whats-new.adoc +++ b/src/reference/antora/modules/ROOT/pages/whats-new.adoc @@ -13,4 +13,9 @@ This version requires Spring Framework 6.2. [[x32-consistent-hash-exchange]] === Consistent Hash Exchange -The convenient `ConsistentHashExchange` and respective `ExchangeBuilder.consistentHashExchange()` API has been introduced. \ No newline at end of file +The convenient `ConsistentHashExchange` and respective `ExchangeBuilder.consistentHashExchange()` API has been introduced. + +[[x32-retry-count-header]] +=== The `retry_count` header + +The `retry_count` header should be used now instead of relying on server side increment for the `x-death.count` property. \ No newline at end of file From 7f714b0d3554858657f728042973bb268406a5fc Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 7 Oct 2024 17:23:08 -0400 Subject: [PATCH 577/737] Remove `getPrefix()` from `ObservationDocumentation` impls --- .../support/micrometer/RabbitListenerObservation.java | 7 ++----- .../support/micrometer/RabbitTemplateObservation.java | 7 ++----- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java index f5f8528491..a9bb6dabd8 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java @@ -27,6 +27,8 @@ * * @author Gary Russell * @author Vincent Meunier + * @author Artem Bilan + * * @since 3.0 */ public enum RabbitListenerObservation implements ObservationDocumentation { @@ -41,11 +43,6 @@ public Class> getDefaultConve return DefaultRabbitListenerObservationConvention.class; } - @Override - public String getPrefix() { - return "spring.rabbit.listener"; - } - @Override public KeyName[] getLowCardinalityKeyNames() { return ListenerLowCardinalityTags.values(); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservation.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservation.java index 6008e1e06d..06a5551878 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservation.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservation.java @@ -27,6 +27,8 @@ * * @author Gary Russell * @author Vincent Meunier + * @author Artem Bilan + * * @since 3.0 * */ @@ -42,11 +44,6 @@ public Class> getDefaultConve return DefaultRabbitTemplateObservationConvention.class; } - @Override - public String getPrefix() { - return "spring.rabbit.template"; - } - @Override public KeyName[] getLowCardinalityKeyNames() { return TemplateLowCardinalityTags.values(); From 1c429b81d965b533b88413fb23e9ddbe988c0d36 Mon Sep 17 00:00:00 2001 From: smallbun <30397655+leshalv@users.noreply.github.com> Date: Wed, 9 Oct 2024 03:56:01 +0800 Subject: [PATCH 578/737] Add `RabbitTemplate.getBeforePublishPostProcessors()` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `RabbitTemplate` class currently provides a `addBeforePublishPostProcessors` method to configure `MessagePostProcessor` instances that can modify messages before they are sent. However, there is no corresponding `getBeforePublishPostProcessors` method to retrieve the currently configured processors. This PR introduces the `getBeforePublishPostProcessors` method, enhancing the flexibility and configurability of `RabbitTemplate`. #### Justification: 1. **Increased Flexibility**: - While `addBeforePublishPostProcessors` allows users to configure message processors, there is currently no way to retrieve or modify the existing `MessagePostProcessor` list. Adding `getBeforePublishPostProcessors` gives users the ability to access and modify these processors dynamically, allowing for greater flexibility in scenarios where message processing needs to be altered based on context. 2. **Support for Multiple `RabbitTemplate` Instances**: - In more complex applications, it's common to create multiple `RabbitTemplate` instances to handle different business logic. For instance, the `ConfirmCallback` mechanism is globally applied, but by manually creating different `RabbitTemplate` instances, users can configure distinct callbacks for each instance. Introducing the `getBeforePublishPostProcessors` method will allow users to retrieve and reuse message processors between different `RabbitTemplate` instances, enhancing flexibility when handling different message routing and confirmation scenarios. 3. **Improved Consistency**: - Spring generally follows a "getter-setter" pattern for its components, and many classes offer both methods for configuring and retrieving values. By providing `getBeforePublishPostProcessors`, the API will become more consistent, following Spring’s design principles and making it easier for developers to interact with the `RabbitTemplate` class. 4. **Better Support for Advanced Use Cases**: - In advanced scenarios where users need to dynamically add or remove message processors based on business needs, the ability to retrieve the current processors simplifies the code. Without this getter method, developers need to manually track and manage `MessagePostProcessor` lists, which adds unnecessary complexity and can lead to duplication of logic. 5. **Enhanced Developer Experience**: - Having the ability to get the current `MessagePostProcessor` list helps developers debug and analyze the message pipeline more easily. This makes it simpler to understand how messages are being transformed before being sent, improving both the developer experience and troubleshooting process. --- .../amqp/rabbit/core/RabbitTemplate.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java index 5d2894c96c..3de6bbe7fb 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java @@ -633,6 +633,18 @@ public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.evaluationContext.addPropertyAccessor(new MapAccessor()); } + /** + * Return configured before post {@link MessagePostProcessor}s or {@code null}. + * @return configured before post {@link MessagePostProcessor}s or {@code null}. + * @since 3.2 + */ + @Nullable + public Collection getBeforePublishPostProcessors() { + return this.beforePublishPostProcessors != null + ? Collections.unmodifiableCollection(this.beforePublishPostProcessors) + : null; + } + /** * Set {@link MessagePostProcessor}s that will be invoked immediately before invoking * {@code Channel#basicPublish()}, after all other processing, except creating the From 8848f3bc16666021d7f87db3139804e33382ad5d Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Wed, 9 Oct 2024 03:00:29 +0700 Subject: [PATCH 579/737] `DefaultMessagePropertiesConverter`: improve conditions We can reduce `else if` condition in method `DefaultMessagePropertiesConverter.convertLongStringIfNecessary()` --- .../DefaultMessagePropertiesConverter.java | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java index 24632abe0c..44ccbdfe0e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java @@ -42,6 +42,7 @@ * @author Soeren Unruh * @author Raylax Grey * @author Artem Bilan + * @author Ngoc Nhan * * @since 1.0 */ @@ -275,27 +276,24 @@ private Object convertLongString(LongString longString, String charset) { * @return the converted string. */ private Object convertLongStringIfNecessary(Object valueArg, String charset) { - Object value = valueArg; - if (value instanceof LongString longStr) { - value = convertLongString(longStr, charset); + if (valueArg instanceof LongString longStr) { + return convertLongString(longStr, charset); } - else if (value instanceof List) { - List convertedList = new ArrayList<>(((List) value).size()); - for (Object listValue : (List) value) { - convertedList.add(this.convertLongStringIfNecessary(listValue, charset)); - } - value = convertedList; + + if (valueArg instanceof List values) { + List convertedList = new ArrayList<>(values.size()); + values.forEach(value -> convertedList.add(this.convertLongStringIfNecessary(value, charset))); + return convertedList; } - else if (value instanceof Map) { - @SuppressWarnings("unchecked") - Map originalMap = (Map) value; + + if (valueArg instanceof Map originalMap) { Map convertedMap = new HashMap<>(); - for (Map.Entry entry : originalMap.entrySet()) { - convertedMap.put(entry.getKey(), this.convertLongStringIfNecessary(entry.getValue(), charset)); - } - value = convertedMap; + originalMap.forEach( + (key, value) -> convertedMap.put((String) key, this.convertLongStringIfNecessary(value, charset))); + return convertedMap; } - return value; + + return valueArg; } } From f5f368daae0f22c99e2e86910a7787280c3b2108 Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Thu, 10 Oct 2024 21:13:10 +0700 Subject: [PATCH 580/737] Simplify the expression in some `equals()` methods --- .../java/org/springframework/amqp/core/Message.java | 12 ++++-------- .../springframework/amqp/core/MessageProperties.java | 10 +++------- .../springframework/amqp/core/QueueInformation.java | 12 ++++-------- 3 files changed, 11 insertions(+), 23 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/Message.java b/spring-amqp/src/main/java/org/springframework/amqp/core/Message.java index 3cb4872102..4039013615 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/Message.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/Message.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -36,6 +36,7 @@ * @author Gary Russell * @author Alex Panchenko * @author Artem Bilan + * @author Ngoc Nhan */ public class Message implements Serializable { @@ -168,14 +169,9 @@ public boolean equals(Object obj) { return false; } if (this.messageProperties == null) { - if (other.messageProperties != null) { - return false; - } - } - else if (!this.messageProperties.equals(other.messageProperties)) { - return false; + return other.messageProperties == null; } - return true; + return this.messageProperties.equals(other.messageProperties); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java index 75a402a4d2..81e4954d4c 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java @@ -36,6 +36,7 @@ * @author Artem Bilan * @author Csaba Soti * @author Raylax Grey + * @author Ngoc Nhan */ public class MessageProperties implements Serializable { @@ -812,14 +813,9 @@ else if (!this.type.equals(other.type)) { return false; } if (this.userId == null) { - if (other.userId != null) { - return false; - } - } - else if (!this.userId.equals(other.userId)) { - return false; + return other.userId == null; } - return true; + return this.userId.equals(other.userId); } @Override // NOSONAR complexity diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/QueueInformation.java b/spring-amqp/src/main/java/org/springframework/amqp/core/QueueInformation.java index de914f9439..f2d8df2453 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/QueueInformation.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/QueueInformation.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-2024 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. @@ -20,6 +20,7 @@ * Information about a queue, resulting from a passive declaration. * * @author Gary Russell + * @author Ngoc Nhan * @since 2.2 * */ @@ -70,14 +71,9 @@ public boolean equals(Object obj) { } QueueInformation other = (QueueInformation) obj; if (this.name == null) { - if (other.name != null) { - return false; - } + return other.name == null; } - else if (!this.name.equals(other.name)) { - return false; - } - return true; + return this.name.equals(other.name); } @Override From 15a53a465907df608ae910f86a85632e7c7f4a52 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 10 Oct 2024 14:20:23 -0400 Subject: [PATCH 581/737] Upgrade deps including Gradle plugins * Remove some deps which are now managed transitively from RabbitMQ `stream-client` --- build.gradle | 27 ++++++++----------- .../amqp/rabbit/core/RabbitTemplate.java | 2 +- .../PublisherCallbackChannelTests.java | 4 +-- .../core/BatchingRabbitTemplateTests.java | 2 +- .../core/RabbitTemplateIntegrationTests.java | 2 +- .../amqp/rabbit/core/RabbitTemplateTests.java | 2 +- .../rabbit/listener/ErrorHandlerTests.java | 4 +-- .../SimpleMessageListenerContainerTests.java | 2 +- .../MessagingMessageListenerAdapterTests.java | 2 +- 9 files changed, 21 insertions(+), 26 deletions(-) diff --git a/build.gradle b/build.gradle index 6f983cea93..eb986c8bc4 100644 --- a/build.gradle +++ b/build.gradle @@ -18,13 +18,13 @@ buildscript { plugins { id 'base' id 'idea' - id 'org.ajoberstar.grgit' version '5.2.2' + id 'org.ajoberstar.grgit' version '5.3.0' id 'io.spring.nohttp' version '0.0.11' id 'io.spring.dependency-management' version '1.1.6' apply false id 'org.antora' version '1.0.0' id 'io.spring.antora.generate-antora-yml' version '0.0.1' id 'com.github.spotbugs' version '6.0.24' - id 'io.freefair.aggregate-javadoc' version '8.6' + id 'io.freefair.aggregate-javadoc' version '8.10.2' } description = 'Spring AMQP' @@ -49,9 +49,9 @@ ext { assertkVersion = '0.28.1' awaitilityVersion = '4.2.2' commonsCompressVersion = '1.27.1' - commonsHttpClientVersion = '5.3.1' + commonsHttpClientVersion = '5.4' commonsPoolVersion = '2.12.0' - hamcrestVersion = '2.2' + hamcrestVersion = '3.0' hibernateValidationVersion = '8.0.1.Final' jacksonBomVersion = '2.18.0' jaywayJsonPathVersion = '2.9.0' @@ -60,20 +60,17 @@ ext { kotlinCoroutinesVersion = '1.8.1' log4jVersion = '2.24.1' logbackVersion = '1.5.8' - lz4Version = '1.8.0' micrometerDocsVersion = '1.0.4' micrometerVersion = '1.14.0-SNAPSHOT' micrometerTracingVersion = '1.4.0-SNAPSHOT' - mockitoVersion = '5.13.0' - rabbitmqStreamVersion = '0.15.0' - rabbitmqVersion = '5.21.0' + mockitoVersion = '5.14.1' + rabbitmqStreamVersion = '0.17.0' + rabbitmqVersion = '5.22.0' reactorVersion = '2024.0.0-SNAPSHOT' - snappyVersion = '1.1.10.7' - springDataVersion = '2024.0.4' + springDataVersion = '2024.1.0-SNAPSHOT' springRetryVersion = '2.0.9' springVersion = '6.2.0-SNAPSHOT' testcontainersVersion = '1.20.2' - zstdJniVersion = '1.5.6-6' javaProjects = subprojects - project(':spring-amqp-bom') } @@ -315,7 +312,7 @@ configure(javaProjects) { subproject -> checkstyle { configDirectory.set(rootProject.file("src/checkstyle")) - toolVersion = '10.8.0' + toolVersion = '10.18.2' } jar { @@ -470,12 +467,10 @@ project('spring-rabbit-stream') { testRuntimeOnly 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' testRuntimeOnly 'com.fasterxml.jackson.module:jackson-module-kotlin' testRuntimeOnly "org.apache.commons:commons-compress:$commonsCompressVersion" - testRuntimeOnly "org.xerial.snappy:snappy-java:$snappyVersion" - testRuntimeOnly "org.lz4:lz4-java:$lz4Version" - testRuntimeOnly "com.github.luben:zstd-jni:$zstdJniVersion" + testImplementation "org.testcontainers:rabbitmq" testImplementation "org.testcontainers:junit-jupiter" - testImplementation "org.apache.logging.log4j:log4j-slf4j-impl:$log4jVersion" + testImplementation 'org.apache.logging.log4j:log4j-slf4j-impl' testImplementation 'org.springframework:spring-webflux' testImplementation 'io.micrometer:micrometer-observation-test' testImplementation 'io.micrometer:micrometer-tracing-bridge-brave' diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java index 3de6bbe7fb..77315c4020 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java @@ -2833,7 +2833,7 @@ public void handleDelivery(String consumerTag, Envelope envelope, BasicPropertie return consumer; } - private static class PendingReply { + private static final class PendingReply { @Nullable private volatile String savedReplyTo; diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelTests.java index 6dcd35b9c6..6872f3f96e 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 the original author or authors. + * Copyright 2019-2024 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. @@ -183,7 +183,7 @@ void confirmAlwaysAfterReturn() throws InterruptedException { assertThat(listener.calls).containsExactly("return", "confirm", "return", "confirm"); } - private static class TheListener implements Listener { + private static final class TheListener implements Listener { private final UUID uuid = UUID.randomUUID(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplateTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplateTests.java index e5587a979f..b554fdd14c 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplateTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplateTests.java @@ -676,7 +676,7 @@ private int getStreamLevel(Object stream) throws Exception { return TestUtils.getPropertyValue(zipStream, "def.level", Integer.class); } - private static class HeaderPostProcessor implements MessagePostProcessor { + private static final class HeaderPostProcessor implements MessagePostProcessor { @Override public Message postProcessMessage(Message message) throws AmqpException { message.getMessageProperties().getHeaders().put("someHeader", "someValue"); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateIntegrationTests.java index 00dea012c1..06109d6606 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateIntegrationTests.java @@ -1733,7 +1733,7 @@ private class PlannedException extends RuntimeException { } @SuppressWarnings("serial") - private class TestTransactionManager extends AbstractPlatformTransactionManager { + private final class TestTransactionManager extends AbstractPlatformTransactionManager { @Override protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException { diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateTests.java index cc8683616e..5e6fb2a6d3 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateTests.java @@ -742,7 +742,7 @@ protected void doRollback(DefaultTransactionStatus status) throws TransactionExc } - private class DoNothingMPP implements MessagePostProcessor { + private static final class DoNothingMPP implements MessagePostProcessor { @Override public Message postProcessMessage(Message message) throws AmqpException { diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ErrorHandlerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ErrorHandlerTests.java index 2d9bcf0947..6ae1f35f18 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ErrorHandlerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ErrorHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2024 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. @@ -127,7 +127,7 @@ private void doTest(Throwable cause) { new MessageProperties()))); } - private static class Foo { + private static final class Foo { @SuppressWarnings("unused") public void foo(String foo) { diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java index f668ba0276..ed4580e603 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java @@ -882,7 +882,7 @@ private void waitForConsumersToStop(Set consumers) { } @SuppressWarnings("serial") - private static class TestTransactionManager extends AbstractPlatformTransactionManager { + private static final class TestTransactionManager extends AbstractPlatformTransactionManager { @Override protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException { diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapterTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapterTests.java index 157e999a0f..f59ae1ce60 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapterTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapterTests.java @@ -488,7 +488,7 @@ public void withHeaders(Foo foo, @Headers Map headers) { } - private static class Foo { + private static final class Foo { private String foo; From 2d3f4b4e993b6ad2f37301f24b43a7dc485156a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 12 Oct 2024 02:04:57 +0000 Subject: [PATCH 582/737] Bump ch.qos.logback:logback-classic from 1.5.8 to 1.5.9 (#2856) Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.8 to 1.5.9. - [Commits](https://github.com/qos-ch/logback/compare/v_1.5.8...v_1.5.9) --- updated-dependencies: - dependency-name: ch.qos.logback:logback-classic dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index eb986c8bc4..a1de461d86 100644 --- a/build.gradle +++ b/build.gradle @@ -59,7 +59,7 @@ ext { junitJupiterVersion = '5.11.2' kotlinCoroutinesVersion = '1.8.1' log4jVersion = '2.24.1' - logbackVersion = '1.5.8' + logbackVersion = '1.5.9' micrometerDocsVersion = '1.0.4' micrometerVersion = '1.14.0-SNAPSHOT' micrometerTracingVersion = '1.4.0-SNAPSHOT' From 09b147900dc2cfb837c9b1eb5b5e17882185258d Mon Sep 17 00:00:00 2001 From: Volodymyr Date: Mon, 14 Oct 2024 16:18:42 +0300 Subject: [PATCH 583/737] Fix typo in sending-messages.adoc --- .../antora/modules/ROOT/pages/amqp/sending-messages.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reference/antora/modules/ROOT/pages/amqp/sending-messages.adoc b/src/reference/antora/modules/ROOT/pages/amqp/sending-messages.adoc index 0d1eaa72da..6ff5f1257d 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/sending-messages.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/sending-messages.adoc @@ -13,7 +13,7 @@ void send(String exchange, String routingKey, Message message) throws AmqpExcept ---- We can begin our discussion with the last method in the preceding listing, since it is actually the most explicit. -It lets an AMQP exchange name (along with a routing key)be provided at runtime. +It lets an AMQP exchange name (along with a routing key) be provided at runtime. The last parameter is the callback that is responsible for actual creating the message instance. An example of using this method to send a message might look like this: The following example shows how to use the `send` method to send a message: From ef23aa9f7f9485ebc50e45a0eb9ed57083c9ca49 Mon Sep 17 00:00:00 2001 From: DongMin <62013201+eyeben@users.noreply.github.com> Date: Tue, 15 Oct 2024 22:37:04 +0900 Subject: [PATCH 584/737] Rename `ConnnectionListenerTests` class without typos --- ...nnectionListenerTests.java => ConnectionListenerTests.java} | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/{ConnnectionListenerTests.java => ConnectionListenerTests.java} (97%) diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnnectionListenerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionListenerTests.java similarity index 97% rename from spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnnectionListenerTests.java rename to spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionListenerTests.java index 03638c6a15..606c71cf4c 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnnectionListenerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionListenerTests.java @@ -27,10 +27,11 @@ /** * @author Gary Russell + * @author DongMin Park * @since 2.2.17 * */ -public class ConnnectionListenerTests { +public class ConnectionListenerTests { @Test void cantConnectCCF() { From 347c96ad2a447a44824eb72a178629287ecea5f2 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 15 Oct 2024 16:42:38 -0400 Subject: [PATCH 585/737] Improve some code in the `AbstractDeclarable` * Add `Declarable.setAdminsThatShouldDeclare(@Nullable)` * Improve `RabbitAdminDeclarationTests` * Fix typo in the `AbstractMessageListenerContainer` JavaDoc --- .../amqp/core/AbstractDeclarable.java | 15 ++-- .../springframework/amqp/core/Declarable.java | 4 +- .../AbstractMessageListenerContainer.java | 2 +- .../core/RabbitAdminDeclarationTests.java | 88 +++++++++---------- 4 files changed, 51 insertions(+), 58 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractDeclarable.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractDeclarable.java index 678620e914..2a18103b5e 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractDeclarable.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractDeclarable.java @@ -25,7 +25,6 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; - import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -40,15 +39,15 @@ */ public abstract class AbstractDeclarable implements Declarable { - private final Lock lock = new ReentrantLock(); + private final Lock lock = new ReentrantLock(); - private boolean shouldDeclare = true; + private final Map arguments; - private Collection declaringAdmins = new ArrayList<>(); + private boolean shouldDeclare = true; private boolean ignoreDeclarationExceptions; - private final Map arguments; + private Collection declaringAdmins = new ArrayList<>(); public AbstractDeclarable() { this(null); @@ -74,7 +73,7 @@ public boolean shouldDeclare() { } /** - * Whether or not this object should be automatically declared + * Whether this object should be automatically declared * by any {@code AmqpAdmin}. Default is {@code true}. * @param shouldDeclare true or false. */ @@ -102,14 +101,14 @@ public void setIgnoreDeclarationExceptions(boolean ignoreDeclarationExceptions) } @Override - public void setAdminsThatShouldDeclare(Object... adminArgs) { + public void setAdminsThatShouldDeclare(@Nullable Object... adminArgs) { Collection admins = new ArrayList<>(); if (adminArgs != null) { if (adminArgs.length > 1) { Assert.noNullElements(adminArgs, "'admins' cannot contain null elements"); } if (adminArgs.length > 0 && !(adminArgs.length == 1 && adminArgs[0] == null)) { - admins.addAll(Arrays.asList(adminArgs)); + admins = Arrays.asList(adminArgs); } } this.declaringAdmins = admins; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/Declarable.java b/spring-amqp/src/main/java/org/springframework/amqp/core/Declarable.java index da6d4788d9..b6948116fc 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/Declarable.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/Declarable.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -61,7 +61,7 @@ public interface Declarable { * the behavior such that all admins will declare the object. * @param adminArgs The admins. */ - void setAdminsThatShouldDeclare(Object... adminArgs); + void setAdminsThatShouldDeclare(@Nullable Object... adminArgs); /** * Add an argument to the declarable. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index 7c3b44618f..80df1dab3d 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -1490,7 +1490,7 @@ protected void invokeErrorHandler(Throwable ex) { // ------------------------------------------------------------------------- /** - * Execute the specified listener, committing or rolling back the transaction afterwards (if necessary). + * Execute the specified listener, committing or rolling back the transaction afterward (if necessary). * @param channel the Rabbit Channel to operate on * @param data the received Rabbit Message * @see #invokeListener diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminDeclarationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminDeclarationTests.java index 3d82f4c14d..fe9b092554 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminDeclarationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminDeclarationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -17,7 +17,7 @@ package org.springframework.amqp.rabbit.core; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyMap; @@ -79,8 +79,8 @@ public void testUnconditional() throws Exception { given(cf.createConnection()).willReturn(conn); given(conn.createChannel(false)).willReturn(channel); given(channel.queueDeclare("foo", true, false, false, new HashMap<>())) - .willReturn(new AMQImpl.Queue.DeclareOk("foo", 0, 0)); - final AtomicReference listener = new AtomicReference(); + .willReturn(new AMQImpl.Queue.DeclareOk("foo", 0, 0)); + AtomicReference listener = new AtomicReference<>(); willAnswer(invocation -> { listener.set((ConnectionListener) invocation.getArguments()[0]); return null; @@ -100,7 +100,7 @@ public void testUnconditional() throws Exception { listener.get().onCreate(conn); verify(channel).queueDeclare("foo", true, false, false, new HashMap<>()); - verify(channel).exchangeDeclare("bar", "direct", true, false, false, new HashMap()); + verify(channel).exchangeDeclare("bar", "direct", true, false, false, new HashMap<>()); verify(channel).queueBind("foo", "bar", "foo", new HashMap<>()); } @@ -108,7 +108,7 @@ public void testUnconditional() throws Exception { public void testNoDeclareWithCachedConnections() throws Exception { com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class); - final List mockChannels = new ArrayList(); + List mockChannels = new ArrayList<>(); AtomicInteger connectionNumber = new AtomicInteger(); willAnswer(invocation -> { @@ -153,8 +153,8 @@ public void testUnconditionalWithExplicitFactory() throws Exception { given(cf.createConnection()).willReturn(conn); given(conn.createChannel(false)).willReturn(channel); given(channel.queueDeclare("foo", true, false, false, new HashMap<>())) - .willReturn(new AMQImpl.Queue.DeclareOk("foo", 0, 0)); - final AtomicReference listener = new AtomicReference(); + .willReturn(new AMQImpl.Queue.DeclareOk("foo", 0, 0)); + AtomicReference listener = new AtomicReference<>(); willAnswer(invocation -> { listener.set(invocation.getArgument(0)); return null; @@ -177,7 +177,7 @@ public void testUnconditionalWithExplicitFactory() throws Exception { listener.get().onCreate(conn); verify(channel).queueDeclare("foo", true, false, false, new HashMap<>()); - verify(channel).exchangeDeclare("bar", "direct", true, false, false, new HashMap()); + verify(channel).exchangeDeclare("bar", "direct", true, false, false, new HashMap<>()); verify(channel).queueBind("foo", "bar", "foo", new HashMap<>()); } @@ -189,8 +189,9 @@ public void testSkipBecauseDifferentFactory() throws Exception { Channel channel = mock(Channel.class); given(cf.createConnection()).willReturn(conn); given(conn.createChannel(false)).willReturn(channel); - given(channel.queueDeclare("foo", true, false, false, null)).willReturn(new AMQImpl.Queue.DeclareOk("foo", 0, 0)); - final AtomicReference listener = new AtomicReference(); + given(channel.queueDeclare("foo", true, false, false, null)) + .willReturn(new AMQImpl.Queue.DeclareOk("foo", 0, 0)); + AtomicReference listener = new AtomicReference<>(); willAnswer(invocation -> { listener.set(invocation.getArgument(0)); return null; @@ -215,20 +216,21 @@ public void testSkipBecauseDifferentFactory() throws Exception { verify(channel, never()).queueDeclare(eq("foo"), anyBoolean(), anyBoolean(), anyBoolean(), any(Map.class)); verify(channel, never()) - .exchangeDeclare(eq("bar"), eq("direct"), anyBoolean(), anyBoolean(), anyBoolean(), any(Map.class)); + .exchangeDeclare(eq("bar"), eq("direct"), anyBoolean(), anyBoolean(), anyBoolean(), any(Map.class)); verify(channel, never()).queueBind(eq("foo"), eq("bar"), eq("foo"), any(Map.class)); } @SuppressWarnings("unchecked") @Test - public void testSkipBecauseShouldntDeclare() throws Exception { + public void testSkipBecauseShouldNotDeclare() throws Exception { ConnectionFactory cf = mock(ConnectionFactory.class); Connection conn = mock(Connection.class); Channel channel = mock(Channel.class); given(cf.createConnection()).willReturn(conn); given(conn.createChannel(false)).willReturn(channel); - given(channel.queueDeclare("foo", true, false, false, null)).willReturn(new AMQImpl.Queue.DeclareOk("foo", 0, 0)); - final AtomicReference listener = new AtomicReference(); + given(channel.queueDeclare("foo", true, false, false, null)) + .willReturn(new AMQImpl.Queue.DeclareOk("foo", 0, 0)); + AtomicReference listener = new AtomicReference<>(); willAnswer(invocation -> { listener.set(invocation.getArgument(0)); return null; @@ -252,7 +254,7 @@ public void testSkipBecauseShouldntDeclare() throws Exception { verify(channel, never()).queueDeclare(eq("foo"), anyBoolean(), anyBoolean(), anyBoolean(), any(Map.class)); verify(channel, never()) - .exchangeDeclare(eq("bar"), eq("direct"), anyBoolean(), anyBoolean(), anyBoolean(), any(Map.class)); + .exchangeDeclare(eq("bar"), eq("direct"), anyBoolean(), anyBoolean(), anyBoolean(), any(Map.class)); verify(channel, never()).queueBind(eq("foo"), eq("bar"), eq("foo"), any(Map.class)); } @@ -263,9 +265,8 @@ public void testJavaConfig() throws Exception { verify(Config.channel1).queueDeclare("foo", true, false, false, new HashMap<>()); verify(Config.channel1, never()).queueDeclare("baz", true, false, false, new HashMap<>()); verify(Config.channel1).queueDeclare("qux", true, false, false, new HashMap<>()); - verify(Config.channel1).exchangeDeclare("bar", "direct", true, false, true, new HashMap()); + verify(Config.channel1).exchangeDeclare("bar", "direct", true, false, true, new HashMap<>()); verify(Config.channel1).queueBind("foo", "bar", "foo", new HashMap<>()); - Config.listener2.onCreate(Config.conn2); verify(Config.channel2, never()) .queueDeclare(eq("foo"), anyBoolean(), anyBoolean(), anyBoolean(), isNull()); @@ -273,9 +274,8 @@ public void testJavaConfig() throws Exception { verify(Config.channel2).queueDeclare("qux", true, false, false, new HashMap<>()); verify(Config.channel2, never()) .exchangeDeclare(eq("bar"), eq("direct"), anyBoolean(), anyBoolean(), - anyBoolean(), anyMap()); + anyBoolean(), anyMap()); verify(Config.channel2, never()).queueBind(eq("foo"), eq("bar"), eq("foo"), anyMap()); - Config.listener3.onCreate(Config.conn3); verify(Config.channel3, never()) .queueDeclare(eq("foo"), anyBoolean(), anyBoolean(), anyBoolean(), isNull()); @@ -286,7 +286,7 @@ public void testJavaConfig() throws Exception { verify(Config.channel3, never()).queueDeclare("qux", true, false, false, new HashMap<>()); verify(Config.channel3, never()) .exchangeDeclare(eq("bar"), eq("direct"), anyBoolean(), anyBoolean(), - anyBoolean(), anyMap()); + anyBoolean(), anyMap()); verify(Config.channel3, never()).queueBind(eq("foo"), eq("bar"), eq("foo"), anyMap()); context.close(); @@ -316,13 +316,9 @@ public void testAddRemove() { assertThat(queue.getDeclaringAdmins()).hasSize(2); queue.setAdminsThatShouldDeclare((Object[]) null); assertThat(queue.getDeclaringAdmins()).hasSize(0); - try { - queue.setAdminsThatShouldDeclare(null, admin1); - fail("Expected Exception"); - } - catch (IllegalArgumentException e) { - assertThat(e.getMessage()).contains("'admins' cannot contain null elements"); - } + assertThatIllegalArgumentException() + .isThrownBy(() -> queue.setAdminsThatShouldDeclare(null, admin1)) + .withMessageContaining("'admins' cannot contain null elements"); } @Test @@ -348,17 +344,17 @@ public void testNoOpWhenNothingToDeclare() throws Exception { @Configuration public static class Config { - private static Connection conn1 = mock(Connection.class); + private static final Connection conn1 = mock(); - private static Connection conn2 = mock(Connection.class); + private static final Connection conn2 = mock(); - private static Connection conn3 = mock(Connection.class); + private static final Connection conn3 = mock(); - private static Channel channel1 = mock(Channel.class); + private static final Channel channel1 = mock(); - private static Channel channel2 = mock(Channel.class); + private static final Channel channel2 = mock(); - private static Channel channel3 = mock(Channel.class); + private static final Channel channel3 = mock(); private static ConnectionListener listener1; @@ -371,9 +367,9 @@ public ConnectionFactory cf1() throws IOException { ConnectionFactory connectionFactory = mock(ConnectionFactory.class); given(connectionFactory.createConnection()).willReturn(conn1); given(conn1.createChannel(false)).willReturn(channel1); - willAnswer(inv -> { - return new AMQImpl.Queue.DeclareOk(inv.getArgument(0), 0, 0); - }).given(channel1).queueDeclare(anyString(), anyBoolean(), anyBoolean(), anyBoolean(), any()); + willAnswer(inv -> new AMQImpl.Queue.DeclareOk(inv.getArgument(0), 0, 0)) + .given(channel1) + .queueDeclare(anyString(), anyBoolean(), anyBoolean(), anyBoolean(), any()); willAnswer(invocation -> { listener1 = invocation.getArgument(0); return null; @@ -386,9 +382,9 @@ public ConnectionFactory cf2() throws IOException { ConnectionFactory connectionFactory = mock(ConnectionFactory.class); given(connectionFactory.createConnection()).willReturn(conn2); given(conn2.createChannel(false)).willReturn(channel2); - willAnswer(inv -> { - return new AMQImpl.Queue.DeclareOk(inv.getArgument(0), 0, 0); - }).given(channel2).queueDeclare(anyString(), anyBoolean(), anyBoolean(), anyBoolean(), any()); + willAnswer(inv -> new AMQImpl.Queue.DeclareOk(inv.getArgument(0), 0, 0)) + .given(channel2) + .queueDeclare(anyString(), anyBoolean(), anyBoolean(), anyBoolean(), any()); willAnswer(invocation -> { listener2 = invocation.getArgument(0); return null; @@ -401,9 +397,9 @@ public ConnectionFactory cf3() throws IOException { ConnectionFactory connectionFactory = mock(ConnectionFactory.class); given(connectionFactory.createConnection()).willReturn(conn3); given(conn3.createChannel(false)).willReturn(channel3); - willAnswer(inv -> { - return new AMQImpl.Queue.DeclareOk(inv.getArgument(0), 0, 0); - }).given(channel3).queueDeclare(anyString(), anyBoolean(), anyBoolean(), anyBoolean(), any()); + willAnswer(inv -> new AMQImpl.Queue.DeclareOk(inv.getArgument(0), 0, 0)) + .given(channel3) + .queueDeclare(anyString(), anyBoolean(), anyBoolean(), anyBoolean(), any()); willAnswer(invocation -> { listener3 = invocation.getArgument(0); return null; @@ -413,14 +409,12 @@ public ConnectionFactory cf3() throws IOException { @Bean public RabbitAdmin admin1() throws IOException { - RabbitAdmin rabbitAdmin = new RabbitAdmin(cf1()); - return rabbitAdmin; + return new RabbitAdmin(cf1()); } @Bean public RabbitAdmin admin2() throws IOException { - RabbitAdmin rabbitAdmin = new RabbitAdmin(cf2()); - return rabbitAdmin; + return new RabbitAdmin(cf2()); } @Bean From d87f2689439768ee7563a899a7b9a6f93d5d9859 Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Wed, 16 Oct 2024 20:57:57 +0700 Subject: [PATCH 586/737] Improve conditions for more code readability * Reduce `else` condition * Improve condition for returning type and avoiding NPE * Polish diamond operator and pattern matching usage --- .../springframework/amqp/core/Address.java | 22 ++++----------- .../springframework/amqp/core/Binding.java | 5 ++-- .../amqp/core/BindingBuilder.java | 12 ++++---- .../converter/AbstractJavaTypeMapper.java | 8 ++---- ...ContentTypeDelegatingMessageConverter.java | 5 ++-- .../MarshallingMessageConverter.java | 10 +++---- .../converter/MessagingMessageConverter.java | 6 ++-- .../converter/RemoteInvocationResult.java | 12 ++++---- .../AbstractDecompressingPostProcessor.java | 6 ++-- .../DelegatingDecompressingPostProcessor.java | 28 +++++++++---------- .../amqp/utils/test/TestUtils.java | 12 ++++---- .../amqp/core/AddressTests.java | 6 +++- 12 files changed, 61 insertions(+), 71 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/Address.java b/spring-amqp/src/main/java/org/springframework/amqp/core/Address.java index c1d10642fb..564c752d16 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/Address.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/Address.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -16,6 +16,7 @@ package org.springframework.amqp.core; +import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -39,6 +40,7 @@ * @author Dave Syer * @author Artem Bilan * @author Gary Russell + * @author Ngoc Nhan */ public class Address { @@ -111,21 +113,9 @@ public String getRoutingKey() { @Override public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - Address address = (Address) o; - - return !(this.exchangeName != null - ? !this.exchangeName.equals(address.exchangeName) - : address.exchangeName != null) - && !(this.routingKey != null - ? !this.routingKey.equals(address.routingKey) - : address.routingKey != null); + return o instanceof Address address + && Objects.equals(this.exchangeName, address.exchangeName) + && Objects.equals(this.routingKey, address.routingKey); } @Override diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/Binding.java b/spring-amqp/src/main/java/org/springframework/amqp/core/Binding.java index a5ee74273f..ac2ac58acd 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/Binding.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/Binding.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -30,6 +30,7 @@ * @author Mark Fisher * @author Dave Syer * @author Gary Russell + * @author Ngoc Nhan * * @see AmqpAdmin */ @@ -74,7 +75,7 @@ public Binding(@Nullable Queue lazyQueue, @Nullable String destination, Destinat String exchange, @Nullable String routingKey, @Nullable Map arguments) { super(arguments); - Assert.isTrue(lazyQueue == null || destinationType.equals(DestinationType.QUEUE), + Assert.isTrue(lazyQueue == null || destinationType == DestinationType.QUEUE, "'lazyQueue' must be null for destination type " + destinationType); Assert.isTrue(lazyQueue != null || destination != null, "`destination` cannot be null"); this.lazyQueue = lazyQueue; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/BindingBuilder.java b/spring-amqp/src/main/java/org/springframework/amqp/core/BindingBuilder.java index 5eceadd960..d297f16d9d 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/BindingBuilder.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/BindingBuilder.java @@ -231,12 +231,12 @@ public static final class TopicExchangeRoutingKeyConfigurer extends AbstractRout public Binding with(String routingKey) { return new Binding(destination.queue, destination.name, destination.type, exchange, routingKey, - Collections.emptyMap()); + Collections.emptyMap()); } public Binding with(Enum routingKeyEnum) { return new Binding(destination.queue, destination.name, destination.type, exchange, - routingKeyEnum.toString(), Collections.emptyMap()); + routingKeyEnum.toString(), Collections.emptyMap()); } } @@ -282,7 +282,7 @@ public Binding and(Map map) { public Binding noargs() { return new Binding(this.configurer.destination.queue, this.configurer.destination.name, this.configurer.destination.type, this.configurer.exchange, - this.routingKey, Collections.emptyMap()); + this.routingKey, Collections.emptyMap()); } } @@ -298,17 +298,17 @@ public static final class DirectExchangeRoutingKeyConfigurer extends AbstractRou public Binding with(String routingKey) { return new Binding(destination.queue, destination.name, destination.type, exchange, routingKey, - Collections.emptyMap()); + Collections.emptyMap()); } public Binding with(Enum routingKeyEnum) { return new Binding(destination.queue, destination.name, destination.type, exchange, - routingKeyEnum.toString(), Collections.emptyMap()); + routingKeyEnum.toString(), Collections.emptyMap()); } public Binding withQueueName() { return new Binding(destination.queue, destination.name, destination.type, exchange, destination.name, - Collections.emptyMap()); + Collections.emptyMap()); } } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJavaTypeMapper.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJavaTypeMapper.java index 76531523be..e19c62dc17 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJavaTypeMapper.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJavaTypeMapper.java @@ -99,11 +99,9 @@ protected String retrieveHeader(MessageProperties properties, String headerName) protected String retrieveHeaderAsString(MessageProperties properties, String headerName) { Map headers = properties.getHeaders(); Object classIdFieldNameValue = headers.get(headerName); - String classId = null; - if (classIdFieldNameValue != null) { - classId = classIdFieldNameValue.toString(); - } - return classId; + return classIdFieldNameValue != null + ? classIdFieldNameValue.toString() + : null; } private void createReverseMap() { diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverter.java index 0fad672ee1..35fb9d901c 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverter.java @@ -109,9 +109,8 @@ protected MessageConverter getConverterForContentType(String contentType) { if (delegate == null) { throw new MessageConversionException("No delegate converter is specified for content type " + contentType); } - else { - return delegate; - } + + return delegate; } } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MarshallingMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MarshallingMessageConverter.java index 25f77c9cd7..6d94890221 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MarshallingMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MarshallingMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -40,6 +40,7 @@ * @author Arjen Poutsma * @author Juergen Hoeller * @author James Carr + * @author Ngoc Nhan * @see org.springframework.amqp.core.AmqpTemplate#convertAndSend(Object) * @see org.springframework.amqp.core.AmqpTemplate#receiveAndConvert() */ @@ -77,10 +78,9 @@ public MarshallingMessageConverter(Marshaller marshaller) { "interface. Please set an Unmarshaller explicitly by using the " + "MarshallingMessageConverter(Marshaller, Unmarshaller) constructor."); } - else { - this.marshaller = marshaller; - this.unmarshaller = (Unmarshaller) marshaller; - } + + this.marshaller = marshaller; + this.unmarshaller = (Unmarshaller) marshaller; } /** diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessagingMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessagingMessageConverter.java index 0c91aac106..5bc545086b 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessagingMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessagingMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * Copyright 2014-2024 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. @@ -40,6 +40,7 @@ * is considered to be a request). * * @author Stephane Nicoll + * @author Ngoc Nhan * @since 1.4 */ public class MessagingMessageConverter implements MessageConverter, InitializingBean { @@ -104,11 +105,10 @@ public void afterPropertiesSet() { public org.springframework.amqp.core.Message toMessage(Object object, MessageProperties messageProperties) throws MessageConversionException { - if (!(object instanceof Message)) { + if (!(object instanceof Message input)) { throw new IllegalArgumentException("Could not convert [" + object + "] - only [" + Message.class.getName() + "] is handled by this converter"); } - Message input = (Message) object; this.headerMapper.fromHeaders(input.getHeaders(), messageProperties); org.springframework.amqp.core.Message amqpMessage = this.payloadConverter.toMessage( input.getPayload(), messageProperties); diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationResult.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationResult.java index 7d08e1d139..6945162895 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationResult.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationResult.java @@ -26,6 +26,7 @@ * * @author Juergen Hoeller * @author Gary Russell + * @author Ngoc Nhan * @since 3.0 */ public class RemoteInvocationResult implements Serializable { @@ -142,16 +143,13 @@ public boolean hasInvocationTargetException() { @Nullable public Object recreate() throws Throwable { if (this.exception != null) { - Throwable exToThrow = this.exception; - if (this.exception instanceof InvocationTargetException invocationTargetException) { - exToThrow = invocationTargetException.getTargetException(); - } + Throwable exToThrow = this.exception instanceof InvocationTargetException invocationTargetException + ? invocationTargetException.getTargetException() + : this.exception; RemoteInvocationUtils.fillInClientStackTraceIfPossible(exToThrow); throw exToThrow; } - else { - return this.value; - } + return this.value; } } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/AbstractDecompressingPostProcessor.java b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/AbstractDecompressingPostProcessor.java index 249f0e25e4..3c87829cff 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/AbstractDecompressingPostProcessor.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/AbstractDecompressingPostProcessor.java @@ -38,6 +38,7 @@ * the final content encoding of the decompressed message. * * @author Gary Russell + * @author Ngoc Nhan * @since 1.4.2 */ public abstract class AbstractDecompressingPostProcessor implements MessagePostProcessor, Ordered { @@ -115,9 +116,8 @@ public Message postProcessMessage(Message message) throws AmqpException { throw new AmqpIOException(e); } } - else { - return message; - } + + return message; } /** diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/DelegatingDecompressingPostProcessor.java b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/DelegatingDecompressingPostProcessor.java index 9474d1b7c8..0651734b8c 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/DelegatingDecompressingPostProcessor.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/DelegatingDecompressingPostProcessor.java @@ -98,22 +98,20 @@ public Message postProcessMessage(Message message) throws AmqpException { if (encoding == null) { return message; } - else { - int delimAt = encoding.indexOf(':'); - if (delimAt < 0) { - delimAt = encoding.indexOf(','); - } - if (delimAt > 0) { - encoding = encoding.substring(0, delimAt); - } - MessagePostProcessor decompressor = this.decompressors.get(encoding); - if (decompressor != null) { - return decompressor.postProcessMessage(message); - } - else { - return message; - } + + int delimAt = encoding.indexOf(':'); + if (delimAt < 0) { + delimAt = encoding.indexOf(','); + } + if (delimAt > 0) { + encoding = encoding.substring(0, delimAt); } + MessagePostProcessor decompressor = this.decompressors.get(encoding); + if (decompressor != null) { + return decompressor.postProcessMessage(message); + } + + return message; } } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/utils/test/TestUtils.java b/spring-amqp/src/main/java/org/springframework/amqp/utils/test/TestUtils.java index 5934aa6a6a..f8c802e558 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/utils/test/TestUtils.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/utils/test/TestUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2019 the original author or authors. + * Copyright 2013-2024 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. @@ -25,6 +25,7 @@ * @author Iwein Fuld * @author Oleg Zhurakousky * @author Gary Russell + * @author Ngoc Nhan * @since 1.2 */ public final class TestUtils { @@ -47,13 +48,14 @@ public static Object getPropertyValue(Object root, String propertyPath) { value = accessor.getPropertyValue(tokens[i]); if (value != null) { accessor = new DirectFieldAccessor(value); + continue; } - else if (i == tokens.length - 1) { + + if (i == tokens.length - 1) { return null; } - else { - throw new IllegalArgumentException("intermediate property '" + tokens[i] + "' is null"); - } + + throw new IllegalArgumentException("intermediate property '" + tokens[i] + "' is null"); } return value; } diff --git a/spring-amqp/src/test/java/org/springframework/amqp/core/AddressTests.java b/spring-amqp/src/test/java/org/springframework/amqp/core/AddressTests.java index feaea573b2..17c24e4130 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/core/AddressTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/core/AddressTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2024 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. @@ -25,6 +25,7 @@ * @author Mark Fisher * @author Artem Bilan * @author Gary Russell + * @author Ngoc Nhan */ public class AddressTests { @@ -100,6 +101,9 @@ public void testDirectReplyTo() { @Test public void testEquals() { assertThat(new Address("foo/bar")).isEqualTo(new Address("foo/bar")); + assertThat(new Address("foo", null)).isEqualTo(new Address("foo", null)); + assertThat(new Address(null, "bar")).isEqualTo(new Address(null, "bar")); + assertThat(new Address(null, null)).isEqualTo(new Address(null, null)); } } From 9873f4a162f4d4ed4af3d63883e3c226b5a838b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 19 Oct 2024 02:10:19 +0000 Subject: [PATCH 587/737] Bump ch.qos.logback:logback-classic from 1.5.9 to 1.5.11 (#2864) Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.9 to 1.5.11. - [Commits](https://github.com/qos-ch/logback/compare/v_1.5.9...v_1.5.11) --- updated-dependencies: - dependency-name: ch.qos.logback:logback-classic dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a1de461d86..c22be696e3 100644 --- a/build.gradle +++ b/build.gradle @@ -59,7 +59,7 @@ ext { junitJupiterVersion = '5.11.2' kotlinCoroutinesVersion = '1.8.1' log4jVersion = '2.24.1' - logbackVersion = '1.5.9' + logbackVersion = '1.5.11' micrometerDocsVersion = '1.0.4' micrometerVersion = '1.14.0-SNAPSHOT' micrometerTracingVersion = '1.4.0-SNAPSHOT' From 0eb13d341aaba51a7d60c174ae2db0a92327e314 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 19 Oct 2024 02:10:28 +0000 Subject: [PATCH 588/737] Bump com.github.spotbugs in the development-dependencies group (#2863) Bumps the development-dependencies group with 1 update: com.github.spotbugs. Updates `com.github.spotbugs` from 6.0.24 to 6.0.25 --- updated-dependencies: - dependency-name: com.github.spotbugs dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c22be696e3..463a995b9c 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ plugins { id 'io.spring.dependency-management' version '1.1.6' apply false id 'org.antora' version '1.0.0' id 'io.spring.antora.generate-antora-yml' version '0.0.1' - id 'com.github.spotbugs' version '6.0.24' + id 'com.github.spotbugs' version '6.0.25' id 'io.freefair.aggregate-javadoc' version '8.10.2' } From dba2ab9037a8e24ad04e88c42ea914173e6b5a33 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 19 Oct 2024 02:10:47 +0000 Subject: [PATCH 589/737] Bump mockitoVersion from 5.14.1 to 5.14.2 (#2865) Bumps `mockitoVersion` from 5.14.1 to 5.14.2. Updates `org.mockito:mockito-core` from 5.14.1 to 5.14.2 - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v5.14.1...v5.14.2) Updates `org.mockito:mockito-junit-jupiter` from 5.14.1 to 5.14.2 - [Release notes](https://github.com/mockito/mockito/releases) - [Commits](https://github.com/mockito/mockito/compare/v5.14.1...v5.14.2) --- updated-dependencies: - dependency-name: org.mockito:mockito-core dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.mockito:mockito-junit-jupiter dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 463a995b9c..0376230c79 100644 --- a/build.gradle +++ b/build.gradle @@ -63,7 +63,7 @@ ext { micrometerDocsVersion = '1.0.4' micrometerVersion = '1.14.0-SNAPSHOT' micrometerTracingVersion = '1.4.0-SNAPSHOT' - mockitoVersion = '5.14.1' + mockitoVersion = '5.14.2' rabbitmqStreamVersion = '0.17.0' rabbitmqVersion = '5.22.0' reactorVersion = '2024.0.0-SNAPSHOT' From f33075257ab082ce987a1799cef83ee390e781fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 19 Oct 2024 02:10:53 +0000 Subject: [PATCH 590/737] Bump org.springframework.retry:spring-retry from 2.0.9 to 2.0.10 (#2866) Bumps [org.springframework.retry:spring-retry](https://github.com/spring-projects/spring-retry) from 2.0.9 to 2.0.10. - [Release notes](https://github.com/spring-projects/spring-retry/releases) - [Commits](https://github.com/spring-projects/spring-retry/compare/v2.0.9...v2.0.10) --- updated-dependencies: - dependency-name: org.springframework.retry:spring-retry dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0376230c79..350d32c759 100644 --- a/build.gradle +++ b/build.gradle @@ -68,7 +68,7 @@ ext { rabbitmqVersion = '5.22.0' reactorVersion = '2024.0.0-SNAPSHOT' springDataVersion = '2024.1.0-SNAPSHOT' - springRetryVersion = '2.0.9' + springRetryVersion = '2.0.10' springVersion = '6.2.0-SNAPSHOT' testcontainersVersion = '1.20.2' From 33da2a6158a49ff62a37cbebc5ac1e8ac5e73839 Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Sat, 19 Oct 2024 05:58:29 -0400 Subject: [PATCH 591/737] Improve conditions in code * Reduce `else` condition and modernize switch pattern * Check condition after calling lock method * Some additional code clean up --- .../stream/producer/RabbitStreamTemplate.java | 88 +++++++++---------- ...itListenerAnnotationBeanPostProcessor.java | 5 +- .../rabbit/batch/SimpleBatchingStrategy.java | 12 ++- .../config/ListenerContainerFactoryBean.java | 15 ++-- .../config/ListenerContainerParser.java | 8 +- ...RetryOperationsInterceptorFactoryBean.java | 31 +++---- .../AbstractRoutingConnectionFactory.java | 4 +- .../connection/ConsumerChannelRegistry.java | 8 +- .../rabbit/connection/PendingConfirm.java | 5 +- .../PooledChannelConnectionFactory.java | 33 ++++--- .../PublisherCallbackChannelImpl.java | 33 ++++--- .../RabbitConnectionFactoryBean.java | 17 ++-- .../amqp/rabbit/connection/RabbitUtils.java | 52 +++++------ .../ThreadChannelConnectionFactory.java | 33 ++++--- .../rabbit/connection/WebFluxNodeLocator.java | 6 +- .../rabbit/listener/MicrometerHolder.java | 5 +- 16 files changed, 162 insertions(+), 193 deletions(-) diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java index 0751e7dbd3..8a42b52101 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors. + * Copyright 2021-2024 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. @@ -55,6 +55,7 @@ * * @author Gary Russell * @author Christian Tzolov + * @author Ngoc Nhan * @since 2.4 * */ @@ -107,29 +108,31 @@ public RabbitStreamTemplate(Environment environment, String streamName) { private Producer createOrGetProducer() { - this.lock.lock(); - try { - if (this.producer == null) { - ProducerBuilder builder = this.environment.producerBuilder(); - if (this.superStreamRouting == null) { - builder.stream(this.streamName); - } - else { - builder.superStream(this.streamName) - .routing(this.superStreamRouting); - } - this.producerCustomizer.accept(this.beanName, builder); - this.producer = builder.build(); - if (!this.streamConverterSet) { - ((DefaultStreamMessageConverter) this.streamConverter).setBuilderSupplier( - () -> this.producer.messageBuilder()); + if (this.producer == null) { + this.lock.lock(); + try { + if (this.producer == null) { + ProducerBuilder builder = this.environment.producerBuilder(); + if (this.superStreamRouting == null) { + builder.stream(this.streamName); + } + else { + builder.superStream(this.streamName) + .routing(this.superStreamRouting); + } + this.producerCustomizer.accept(this.beanName, builder); + this.producer = builder.build(); + if (!this.streamConverterSet) { + ((DefaultStreamMessageConverter) this.streamConverter).setBuilderSupplier( + () -> this.producer.messageBuilder()); + } } } - return this.producer; - } - finally { - this.lock.unlock(); + finally { + this.lock.unlock(); + } } + return this.producer; } @Override @@ -305,24 +308,13 @@ private ConfirmationHandler handleConfirm(CompletableFuture future, Obs } else { int code = confStatus.getCode(); - String errorMessage; - switch (code) { - case Constants.CODE_MESSAGE_ENQUEUEING_FAILED: - errorMessage = "Message Enqueueing Failed"; - break; - case Constants.CODE_PRODUCER_CLOSED: - errorMessage = "Producer Closed"; - break; - case Constants.CODE_PRODUCER_NOT_AVAILABLE: - errorMessage = "Producer Not Available"; - break; - case Constants.CODE_PUBLISH_CONFIRM_TIMEOUT: - errorMessage = "Publish Confirm Timeout"; - break; - default: - errorMessage = "Unknown code: " + code; - break; - } + String errorMessage = switch (code) { + case Constants.CODE_MESSAGE_ENQUEUEING_FAILED -> "Message Enqueueing Failed"; + case Constants.CODE_PRODUCER_CLOSED -> "Producer Closed"; + case Constants.CODE_PRODUCER_NOT_AVAILABLE -> "Producer Not Available"; + case Constants.CODE_PUBLISH_CONFIRM_TIMEOUT -> "Publish Confirm Timeout"; + default -> "Unknown code: " + code; + }; StreamSendException ex = new StreamSendException(errorMessage, code); observation.error(ex); observation.stop(); @@ -339,15 +331,17 @@ private ConfirmationHandler handleConfirm(CompletableFuture future, Obs */ @Override public void close() { - this.lock.lock(); - try { - if (this.producer != null) { - this.producer.close(); - this.producer = null; + if (this.producer != null) { + this.lock.lock(); + try { + if (this.producer != null) { + this.producer.close(); + this.producer = null; + } + } + finally { + this.lock.unlock(); } - } - finally { - this.lock.unlock(); } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java index 2c1ce24447..e22e1b3860 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java @@ -76,6 +76,7 @@ import org.springframework.context.expression.StandardBeanExpressionResolver; import org.springframework.core.Ordered; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.core.convert.ConversionService; @@ -357,7 +358,7 @@ else if (source instanceof Method method) { } return !name.contains("$MockitoMock$"); }) - .map(ann -> ann.synthesize()) + .map(MergedAnnotation::synthesize) .collect(Collectors.toList()); } @@ -893,7 +894,7 @@ private void addToMap(Map map, String key, Object value, Class= this.bufferLimit) { + if (this.currentSize >= this.bufferLimit) { // release immediately, we're already over the limit return new Date(); } - else { - return new Date(System.currentTimeMillis() + this.timeout); - } + + return new Date(System.currentTimeMillis() + this.timeout); } @Override @@ -122,9 +121,8 @@ public Collection releaseBatches() { if (batch == null) { return Collections.emptyList(); } - else { - return Collections.singletonList(batch); - } + + return Collections.singletonList(batch); } private MessageBatch doReleaseBatch() { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java index c919105ea1..b2bc26fb8a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java @@ -575,14 +575,13 @@ private AbstractMessageListenerContainer createContainer() { .acceptIfNotNull(this.retryDeclarationInterval, container::setRetryDeclarationInterval); return container; } - else { - DirectMessageListenerContainer container = new DirectMessageListenerContainer(this.connectionFactory); - JavaUtils.INSTANCE - .acceptIfNotNull(this.consumersPerQueue, container::setConsumersPerQueue) - .acceptIfNotNull(this.taskScheduler, container::setTaskScheduler) - .acceptIfNotNull(this.monitorInterval, container::setMonitorInterval); - return container; - } + + DirectMessageListenerContainer container = new DirectMessageListenerContainer(this.connectionFactory); + JavaUtils.INSTANCE + .acceptIfNotNull(this.consumersPerQueue, container::setConsumersPerQueue) + .acceptIfNotNull(this.taskScheduler, container::setTaskScheduler) + .acceptIfNotNull(this.monitorInterval, container::setMonitorInterval); + return container; } @Override diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerParser.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerParser.java index fce31f9935..71acf60981 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerParser.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerParser.java @@ -101,8 +101,8 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { } List childElements = DomUtils.getChildElementsByTagName(element, LISTENER_ELEMENT); - for (int i = 0; i < childElements.size(); i++) { - parseListener(childElements.get(i), element, parserContext, containerList); + for (Element childElement : childElements) { + parseListener(childElement, element, parserContext, containerList); } parserContext.popAndRegisterContainingComponent(); @@ -190,8 +190,8 @@ private void parseListener(Element listenerEle, Element containerEle, ParserCont else { String[] names = StringUtils.commaDelimitedListToStringArray(queues); List values = new ManagedList<>(); - for (int i = 0; i < names.length; i++) { - values.add(new RuntimeBeanReference(names[i].trim())); + for (String name : names) { + values.add(new RuntimeBeanReference(name.trim())); } containerDef.getPropertyValues().add("queues", values); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatefulRetryOperationsInterceptorFactoryBean.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatefulRetryOperationsInterceptorFactoryBean.java index 3aa40f94ef..4a468f750f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatefulRetryOperationsInterceptorFactoryBean.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatefulRetryOperationsInterceptorFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -27,12 +27,14 @@ import org.springframework.amqp.rabbit.retry.MessageKeyGenerator; import org.springframework.amqp.rabbit.retry.MessageRecoverer; import org.springframework.amqp.rabbit.retry.NewMessageIdentifier; +import org.springframework.lang.Nullable; import org.springframework.retry.RetryOperations; import org.springframework.retry.interceptor.MethodArgumentsKeyGenerator; import org.springframework.retry.interceptor.MethodInvocationRecoverer; import org.springframework.retry.interceptor.NewMethodArgumentsIdentifier; import org.springframework.retry.interceptor.StatefulRetryOperationsInterceptor; import org.springframework.retry.support.RetryTemplate; +import org.springframework.util.Assert; /** * Convenient factory bean for creating a stateful retry interceptor for use in a message listener container, giving you @@ -47,6 +49,7 @@ * * @author Dave Syer * @author Gary Russell + * @author Ngoc Nhan * * @see RetryOperations#execute(org.springframework.retry.RetryCallback, org.springframework.retry.RecoveryCallback, * org.springframework.retry.RetryState) @@ -90,9 +93,8 @@ private NewMethodArgumentsIdentifier createNewItemIdentifier() { if (StatefulRetryOperationsInterceptorFactoryBean.this.newMessageIdentifier == null) { return !message.getMessageProperties().isRedelivered(); } - else { - return StatefulRetryOperationsInterceptorFactoryBean.this.newMessageIdentifier.isNew(message); - } + + return StatefulRetryOperationsInterceptorFactoryBean.this.newMessageIdentifier.isNew(message); }; } @@ -120,6 +122,7 @@ else if (arg instanceof List && messageRecoverer instanceof MessageBatchRecovere private MethodArgumentsKeyGenerator createKeyGenerator() { return args -> { Message message = argToMessage(args); + Assert.notNull(message, "The 'args' must not convert to null"); if (StatefulRetryOperationsInterceptorFactoryBean.this.messageKeyGenerator == null) { String messageId = message.getMessageProperties().getMessageId(); if (messageId == null && message.getMessageProperties().isRedelivered()) { @@ -127,23 +130,20 @@ private MethodArgumentsKeyGenerator createKeyGenerator() { } return messageId; } - else { - return StatefulRetryOperationsInterceptorFactoryBean.this.messageKeyGenerator.getKey(message); - } + return StatefulRetryOperationsInterceptorFactoryBean.this.messageKeyGenerator.getKey(message); }; } - @SuppressWarnings("unchecked") + @Nullable private Message argToMessage(Object[] args) { Object arg = args[1]; - Message message = null; if (arg instanceof Message msg) { - message = msg; + return msg; } - else if (arg instanceof List) { - message = ((List) arg).get(0); + if (arg instanceof List list) { + return (Message) list.get(0); } - return message; + return null; } @Override @@ -151,9 +151,4 @@ public Class getObjectType() { return StatefulRetryOperationsInterceptor.class; } - @Override - public boolean isSingleton() { - return true; - } - } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.java index b4fed509e0..be70f77cc5 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.java @@ -69,7 +69,7 @@ public void setTargetConnectionFactories(Map targetCo Assert.noNullElements(targetConnectionFactories.values().toArray(), "'targetConnectionFactories' cannot have null values."); this.targetConnectionFactories.putAll(targetConnectionFactories); - targetConnectionFactories.values().stream().forEach(cf -> checkConfirmsAndReturns(cf)); + targetConnectionFactories.values().forEach(this::checkConfirmsAndReturns); } /** @@ -293,7 +293,7 @@ public void destroy() { @Override public void resetConnection() { - this.targetConnectionFactories.values().forEach(factory -> factory.resetConnection()); + this.targetConnectionFactories.values().forEach(ConnectionFactory::resetConnection); this.defaultTargetConnectionFactory.resetConnection(); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConsumerChannelRegistry.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConsumerChannelRegistry.java index d1a82f4d87..e72c338b04 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConsumerChannelRegistry.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConsumerChannelRegistry.java @@ -84,11 +84,9 @@ public static void unRegisterConsumerChannel() { @Nullable public static Channel getConsumerChannel() { ChannelHolder channelHolder = consumerChannel.get(); - Channel channel = null; - if (channelHolder != null) { - channel = channelHolder.getChannel(); - } - return channel; + return channelHolder != null + ? channelHolder.getChannel() + : null; } /** diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PendingConfirm.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PendingConfirm.java index 95c71fe744..5aa67760c9 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PendingConfirm.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PendingConfirm.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -27,6 +27,7 @@ * expired. It also holds {@link CorrelationData} for * the client to correlate a confirm with a sent message. * @author Gary Russell + * @author Ngoc Nhan * @since 1.0.1 * */ @@ -115,7 +116,7 @@ public void setReturned(boolean isReturned) { * @since 2.2.10 */ public boolean waitForReturnIfNeeded() throws InterruptedException { - return this.returned ? this.latch.await(RETURN_CALLBACK_TIMEOUT, TimeUnit.SECONDS) : true; + return !this.returned || this.latch.await(RETURN_CALLBACK_TIMEOUT, TimeUnit.SECONDS); } /** diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java index a458be72ab..321c8925ab 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java @@ -55,6 +55,7 @@ * @author Gary Russell * @author Leonardo Ferreira * @author Christian Tzolov + * @author Ngoc Nhan * @since 2.3 * */ @@ -255,23 +256,21 @@ private Channel createProxy(Channel channel, boolean transacted) { Advice advice = (MethodInterceptor) invocation -> { String method = invocation.getMethod().getName(); - switch (method) { - case "close": - handleClose(channel, transacted, proxy); - return null; - case "getTargetChannel": - return channel; - case "isTransactional": - return transacted; - case "confirmSelect": - confirmSelected.set(true); - return channel.confirmSelect(); - case "isConfirmSelected": - return confirmSelected.get(); - case "isPublisherConfirms": - return false; - } - return null; + return switch (method) { + case "close" -> { + handleClose(channel, transacted, proxy); + yield null; + } + case "getTargetChannel" -> channel; + case "isTransactional" -> transacted; + case "confirmSelect" -> { + confirmSelected.set(true); + yield channel.confirmSelect(); + } + case "isConfirmSelected" -> confirmSelected.get(); + case "isPublisherConfirms" -> false; + default -> null; + }; }; NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor(advice); advisor.addMethodName("close"); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java index b8ee5e29dd..74fb85bc95 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java @@ -922,27 +922,26 @@ public Collection expire(Listener listener, long cutoffTime) { try { SortedMap pendingConfirmsForListener = this.pendingConfirms.get(listener); if (pendingConfirmsForListener == null) { - return Collections.emptyList(); + return Collections.emptyList(); } - else { - List expired = new ArrayList<>(); - Iterator> iterator = pendingConfirmsForListener.entrySet().iterator(); - while (iterator.hasNext()) { - PendingConfirm pendingConfirm = iterator.next().getValue(); - if (pendingConfirm.getTimestamp() < cutoffTime) { - expired.add(pendingConfirm); - iterator.remove(); - CorrelationData correlationData = pendingConfirm.getCorrelationData(); - if (correlationData != null && StringUtils.hasText(correlationData.getId())) { - this.pendingReturns.remove(correlationData.getId()); // NOSONAR never null - } - } - else { - break; + + List expired = new ArrayList<>(); + Iterator> iterator = pendingConfirmsForListener.entrySet().iterator(); + while (iterator.hasNext()) { + PendingConfirm pendingConfirm = iterator.next().getValue(); + if (pendingConfirm.getTimestamp() < cutoffTime) { + expired.add(pendingConfirm); + iterator.remove(); + CorrelationData correlationData = pendingConfirm.getCorrelationData(); + if (correlationData != null && StringUtils.hasText(correlationData.getId())) { + this.pendingReturns.remove(correlationData.getId()); // NOSONAR never null } } - return expired; + else { + break; + } } + return expired; } finally { this.lock.unlock(); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitConnectionFactoryBean.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitConnectionFactoryBean.java index d12361ee1d..779bdef7c9 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitConnectionFactoryBean.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitConnectionFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -79,6 +79,7 @@ * @author Hareendran * @author Dominique Villard * @author Zachary DeLuca + * @author Ngoc Nhan * * @since 1.4 */ @@ -360,12 +361,11 @@ protected String getKeyStoreType() { if (this.keyStoreType == null && this.sslProperties.getProperty(KEY_STORE_TYPE) == null) { return KEY_STORE_DEFAULT_TYPE; } - else if (this.keyStoreType != null) { + if (this.keyStoreType != null) { return this.keyStoreType; } - else { - return this.sslProperties.getProperty(KEY_STORE_TYPE); - } + + return this.sslProperties.getProperty(KEY_STORE_TYPE); } /** @@ -389,12 +389,11 @@ protected String getTrustStoreType() { if (this.trustStoreType == null && this.sslProperties.getProperty(TRUST_STORE_TYPE) == null) { return TRUST_STORE_DEFAULT_TYPE; } - else if (this.trustStoreType != null) { + if (this.trustStoreType != null) { return this.trustStoreType; } - else { - return this.sslProperties.getProperty(TRUST_STORE_TYPE); - } + + return this.sslProperties.getProperty(TRUST_STORE_TYPE); } /** diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java index 1d0c1609f8..e135d365a2 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java @@ -228,12 +228,7 @@ public static void setPhysicalCloseRequired(Channel channel, boolean b) { */ public static boolean isPhysicalCloseRequired() { Boolean mustClose = physicalCloseRequired.get(); - if (mustClose == null) { - return false; - } - else { - return mustClose; - } + return mustClose != null && mustClose; } /** @@ -322,13 +317,12 @@ public static boolean isMismatchedQueueArgs(Exception e) { if (sig == null) { return false; } - else { - Method shutdownReason = sig.getReason(); - return shutdownReason instanceof AMQP.Channel.Close closeReason - && AMQP.PRECONDITION_FAILED == closeReason.getReplyCode() - && closeReason.getClassId() == QUEUE_CLASS_ID_50 - && closeReason.getMethodId() == DECLARE_METHOD_ID_10; - } + + Method shutdownReason = sig.getReason(); + return shutdownReason instanceof AMQP.Channel.Close closeReason + && AMQP.PRECONDITION_FAILED == closeReason.getReplyCode() + && closeReason.getClassId() == QUEUE_CLASS_ID_50 + && closeReason.getMethodId() == DECLARE_METHOD_ID_10; } /** @@ -352,13 +346,12 @@ public static boolean isExchangeDeclarationFailure(Exception e) { if (sig == null) { return false; } - else { - Method shutdownReason = sig.getReason(); - return shutdownReason instanceof AMQP.Channel.Close closeReason - && AMQP.PRECONDITION_FAILED == closeReason.getReplyCode() - && closeReason.getClassId() == EXCHANGE_CLASS_ID_40 - && closeReason.getMethodId() == DECLARE_METHOD_ID_10; - } + + Method shutdownReason = sig.getReason(); + return shutdownReason instanceof AMQP.Channel.Close closeReason + && AMQP.PRECONDITION_FAILED == closeReason.getReplyCode() + && closeReason.getClassId() == EXCHANGE_CLASS_ID_40 + && closeReason.getMethodId() == DECLARE_METHOD_ID_10; } /** @@ -395,18 +388,13 @@ public static int getMaxFrame(ConnectionFactory connectionFactory) { public static SaslConfig stringToSaslConfig(String saslConfig, com.rabbitmq.client.ConnectionFactory connectionFactory) { - switch (saslConfig) { - case "DefaultSaslConfig.PLAIN": - return DefaultSaslConfig.PLAIN; - case "DefaultSaslConfig.EXTERNAL": - return DefaultSaslConfig.EXTERNAL; - case "JDKSaslConfig": - return new JDKSaslConfig(connectionFactory); - case "CRDemoSaslConfig": - return new CRDemoMechanism.CRDemoSaslConfig(); - default: - throw new IllegalStateException("Unrecognized SaslConfig: " + saslConfig); - } + return switch (saslConfig) { + case "DefaultSaslConfig.PLAIN" -> DefaultSaslConfig.PLAIN; + case "DefaultSaslConfig.EXTERNAL" -> DefaultSaslConfig.EXTERNAL; + case "JDKSaslConfig" -> new JDKSaslConfig(connectionFactory); + case "CRDemoSaslConfig" -> new CRDemoMechanism.CRDemoSaslConfig(); + default -> throw new IllegalStateException("Unrecognized SaslConfig: " + saslConfig); + }; } /** diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java index b9d371e683..8097eed059 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java @@ -24,7 +24,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; -import java.util.stream.Collectors; import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInterceptor; @@ -196,8 +195,8 @@ public void destroy() { this.logger.warn("Unclaimed context switches from threads:" + this.switchesInProgress.values() .stream() - .map(t -> t.getName()) - .collect(Collectors.toList())); + .map(Thread::getName) + .toList()); } this.contextSwitches.clear(); this.switchesInProgress.clear(); @@ -319,23 +318,21 @@ private Channel createProxy(Channel channel, boolean transactional) { Advice advice = (MethodInterceptor) invocation -> { String method = invocation.getMethod().getName(); - switch (method) { - case "close": + return switch (method) { + case "close" -> { handleClose(channel, transactional); - return null; - case "getTargetChannel": - return channel; - case "isTransactional": - return transactional; - case "confirmSelect": + yield null; + } + case "getTargetChannel" -> channel; + case "isTransactional" -> transactional; + case "confirmSelect" -> { confirmSelected.set(true); - return channel.confirmSelect(); - case "isConfirmSelected": - return confirmSelected.get(); - case "isPublisherConfirms": - return false; - } - return null; + yield channel.confirmSelect(); + } + case "isConfirmSelected" -> confirmSelected.get(); + case "isPublisherConfirms" -> false; + default -> null; + }; }; NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor(advice); advisor.addMethodName("close"); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/WebFluxNodeLocator.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/WebFluxNodeLocator.java index 6c10164474..75acd71e81 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/WebFluxNodeLocator.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/WebFluxNodeLocator.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2024 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. @@ -34,6 +34,7 @@ * A {@link NodeLocator} using the Spring WebFlux {@link WebClient}. * * @author Gary Russell + * @author Ngoc Nhan * @since 2.4.8 * */ @@ -46,14 +47,13 @@ public Map restCall(WebClient client, String baseUri, String vho URI uri = new URI(baseUri) .resolve("/api/queues/" + UriUtils.encodePathSegment(vhost, StandardCharsets.UTF_8) + "/" + queue); - HashMap queueInfo = client.get() + return client.get() .uri(uri) .accept(MediaType.APPLICATION_JSON) .retrieve() .bodyToMono(new ParameterizedTypeReference>() { }) .block(Duration.ofSeconds(10)); // NOSONAR magic# - return queueInfo != null ? queueInfo : null; } /** diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MicrometerHolder.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MicrometerHolder.java index 72d06e140f..5cd536329f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MicrometerHolder.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MicrometerHolder.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2024 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. @@ -33,6 +33,7 @@ * Abstraction to avoid hard reference to Micrometer. * * @author Gary Russell + * @author Ngoc Nhan * @since 2.4.6 * */ @@ -95,7 +96,7 @@ private Timer buildTimer(String aListenerId, String result, String queue, String .tag("result", result) .tag("exception", exception); if (this.tags != null && !this.tags.isEmpty()) { - this.tags.forEach((key, value) -> builder.tag(key, value)); + this.tags.forEach(builder::tag); } Timer registeredTimer = builder.register(this.registry); this.timers.put(queue + exception, registeredTimer); From 24fcddb104747ed2e3937e3502c219148604ef32 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 16:49:37 +0000 Subject: [PATCH 592/737] Bump org.junit:junit-bom from 5.11.2 to 5.11.3 (#2875) Bumps [org.junit:junit-bom](https://github.com/junit-team/junit5) from 5.11.2 to 5.11.3. - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.11.2...r5.11.3) --- updated-dependencies: - dependency-name: org.junit:junit-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 350d32c759..6a5237aeed 100644 --- a/build.gradle +++ b/build.gradle @@ -56,7 +56,7 @@ ext { jacksonBomVersion = '2.18.0' jaywayJsonPathVersion = '2.9.0' junit4Version = '4.13.2' - junitJupiterVersion = '5.11.2' + junitJupiterVersion = '5.11.3' kotlinCoroutinesVersion = '1.8.1' log4jVersion = '2.24.1' logbackVersion = '1.5.11' From 94956d6f73746085996c5e210741b9467ab00b40 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 21 Oct 2024 13:06:22 -0400 Subject: [PATCH 593/737] Upgrade deps; including Antora NodeJS modules * Prepare for release * Remove redundant `org.apache.commons:commons-compress` dep. Managed now transitively by the `com.rabbitmq:stream-client` library --- build.gradle | 24 +++++++++++------------- src/reference/antora/antora-playbook.yml | 2 +- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/build.gradle b/build.gradle index 6a5237aeed..0d2197b5af 100644 --- a/build.gradle +++ b/build.gradle @@ -48,7 +48,6 @@ ext { assertjVersion = '3.26.3' assertkVersion = '0.28.1' awaitilityVersion = '4.2.2' - commonsCompressVersion = '1.27.1' commonsHttpClientVersion = '5.4' commonsPoolVersion = '2.12.0' hamcrestVersion = '3.0' @@ -61,15 +60,15 @@ ext { log4jVersion = '2.24.1' logbackVersion = '1.5.11' micrometerDocsVersion = '1.0.4' - micrometerVersion = '1.14.0-SNAPSHOT' - micrometerTracingVersion = '1.4.0-SNAPSHOT' + micrometerVersion = '1.14.0-RC1' + micrometerTracingVersion = '1.4.0-RC1' mockitoVersion = '5.14.2' - rabbitmqStreamVersion = '0.17.0' + rabbitmqStreamVersion = '0.18.0' rabbitmqVersion = '5.22.0' - reactorVersion = '2024.0.0-SNAPSHOT' - springDataVersion = '2024.1.0-SNAPSHOT' + reactorVersion = '2024.0.0-RC1' + springDataVersion = '2024.1.0-RC1' springRetryVersion = '2.0.10' - springVersion = '6.2.0-SNAPSHOT' + springVersion = '6.2.0-RC2' testcontainersVersion = '1.20.2' javaProjects = subprojects - project(':spring-amqp-bom') @@ -80,11 +79,11 @@ antora { playbook = file('src/reference/antora/antora-playbook.yml') options = ['to-dir' : project.layout.buildDirectory.dir('site').get().toString(), clean: true, fetch: !project.gradle.startParameter.offline, stacktrace: true] dependencies = [ - '@antora/atlas-extension': '1.0.0-alpha.1', - '@antora/collector-extension': '1.0.0-alpha.3', - '@asciidoctor/tabs': '1.0.0-beta.3', - '@springio/antora-extensions': '1.11.1', - '@springio/asciidoctor-extensions': '1.0.0-alpha.10', + '@antora/atlas-extension': '1.0.0-alpha.2', + '@antora/collector-extension': '1.0.0-beta.3', + '@asciidoctor/tabs': '1.0.0-beta.6', + '@springio/antora-extensions': '1.14.2', + '@springio/asciidoctor-extensions': '1.0.0-alpha.14', ] } @@ -466,7 +465,6 @@ project('spring-rabbit-stream') { testRuntimeOnly 'com.fasterxml.jackson.core:jackson-databind' testRuntimeOnly 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' testRuntimeOnly 'com.fasterxml.jackson.module:jackson-module-kotlin' - testRuntimeOnly "org.apache.commons:commons-compress:$commonsCompressVersion" testImplementation "org.testcontainers:rabbitmq" testImplementation "org.testcontainers:junit-jupiter" diff --git a/src/reference/antora/antora-playbook.yml b/src/reference/antora/antora-playbook.yml index 333dfc2fb5..8397e13ecc 100644 --- a/src/reference/antora/antora-playbook.yml +++ b/src/reference/antora/antora-playbook.yml @@ -32,4 +32,4 @@ runtime: format: pretty ui: bundle: - url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.15/ui-bundle.zip + url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.17/ui-bundle.zip From b8a0f7acfc5144d9d2b680addae1968711f8fbee Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 21 Oct 2024 18:32:09 +0000 Subject: [PATCH 594/737] [artifactory-release] Release version 3.2.0-RC1 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 6bc0422ab1..70c15ef48c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.2.0-SNAPSHOT +version=3.2.0-RC1 org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true From b1c6893e3103ce83b0f937aba2601f36f7a54f75 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 21 Oct 2024 18:32:11 +0000 Subject: [PATCH 595/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 70c15ef48c..6bc0422ab1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.2.0-RC1 +version=3.2.0-SNAPSHOT org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true From f150bf531ae9cf0f8ae7e278889e7f9b084ffee6 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 21 Oct 2024 17:47:49 -0400 Subject: [PATCH 596/737] Make `RabbitStreamTemplate.producer` as `volatile` --- .../rabbit/stream/producer/RabbitStreamTemplate.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java index 8a42b52101..a0bde53ad2 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java @@ -79,8 +79,6 @@ public class RabbitStreamTemplate implements RabbitStreamOperations, Application private boolean streamConverterSet; - private Producer producer; - private String beanName; private ProducerCustomizer producerCustomizer = (name, builder) -> { }; @@ -90,10 +88,12 @@ public class RabbitStreamTemplate implements RabbitStreamOperations, Application @Nullable private RabbitStreamTemplateObservationConvention observationConvention; - private volatile boolean observationRegistryObtained; - private ObservationRegistry observationRegistry; + private volatile Producer producer; + + private volatile boolean observationRegistryObtained; + /** * Construct an instance with the provided {@link Environment}. * @param environment the environment. From c321e948903f42bcaa3e8c1a78471fb71a4bf853 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 22 Oct 2024 11:50:32 -0400 Subject: [PATCH 597/737] Increase `await()` timeout to 30 sec for `AsyncRabbitTemplateTests` --- .../amqp/rabbit/AsyncRabbitTemplateTests.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java index e054a7abab..b0cb6d92c1 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java @@ -22,6 +22,7 @@ import static org.awaitility.Awaitility.await; import static org.mockito.Mockito.mock; +import java.time.Duration; import java.util.Map; import java.util.UUID; import java.util.concurrent.CancellationException; @@ -35,6 +36,8 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Address; @@ -91,6 +94,11 @@ public class AsyncRabbitTemplateTests { private final Message fooMessage = new SimpleMessageConverter().toMessage("foo", new MessageProperties()); + @BeforeAll + static void setup() { + Awaitility.setDefaultTimeout(Duration.ofSeconds(30)); + } + @Test public void testConvert1Arg() throws Exception { final AtomicBoolean mppCalled = new AtomicBoolean(); From b391195b50518e2add0f3d2d225db57b035bbaf4 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 23 Oct 2024 14:36:30 -0400 Subject: [PATCH 598/737] Upgrade reusable WFs from `main` to `v4` **Auto-cherry-pick to `3.1.x`** --- .github/workflows/announce-milestone-planning.yml | 2 +- .github/workflows/merge-dependabot-pr.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/announce-milestone-planning.yml b/.github/workflows/announce-milestone-planning.yml index e4e90710c9..1075304cd1 100644 --- a/.github/workflows/announce-milestone-planning.yml +++ b/.github/workflows/announce-milestone-planning.yml @@ -6,6 +6,6 @@ on: jobs: announce-milestone-planning: - uses: spring-io/spring-github-workflows/.github/workflows/spring-announce-milestone-planning.yml@main + uses: spring-io/spring-github-workflows/.github/workflows/spring-announce-milestone-planning.yml@v4 secrets: SPRING_RELEASE_CHAT_WEBHOOK_URL: ${{ secrets.SPRING_RELEASE_GCHAT_WEBHOOK_URL }} \ No newline at end of file diff --git a/.github/workflows/merge-dependabot-pr.yml b/.github/workflows/merge-dependabot-pr.yml index 0b1d927d8e..44a74e507b 100644 --- a/.github/workflows/merge-dependabot-pr.yml +++ b/.github/workflows/merge-dependabot-pr.yml @@ -12,7 +12,7 @@ jobs: merge-dependabot-pr: permissions: write-all - uses: spring-io/spring-github-workflows/.github/workflows/spring-merge-dependabot-pr.yml@main + uses: spring-io/spring-github-workflows/.github/workflows/spring-merge-dependabot-pr.yml@v4 with: mergeArguments: --auto --squash autoMergeSnapshots: true \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 90f17f1609..40dfecca8b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: contents: write issues: write - uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-release.yml@main + uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-release.yml@v4 secrets: GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} From e48445af516dcf12697d1cac994c8ffdece6b8e4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 26 Oct 2024 02:26:09 +0000 Subject: [PATCH 599/737] Bump spring-io/spring-github-workflows (#2877) Bumps the development-dependencies group with 1 update: [spring-io/spring-github-workflows](https://github.com/spring-io/spring-github-workflows). Updates `spring-io/spring-github-workflows` from 3 to 4 - [Release notes](https://github.com/spring-io/spring-github-workflows/releases) - [Commits](https://github.com/spring-io/spring-github-workflows/compare/v3...v4) --- updated-dependencies: - dependency-name: spring-io/spring-github-workflows dependency-type: direct:production update-type: version-update:semver-major dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/auto-cherry-pick.yml | 2 +- .github/workflows/backport-issue.yml | 2 +- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/pr-build.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/auto-cherry-pick.yml b/.github/workflows/auto-cherry-pick.yml index ad08e22428..bb476cb186 100644 --- a/.github/workflows/auto-cherry-pick.yml +++ b/.github/workflows/auto-cherry-pick.yml @@ -8,6 +8,6 @@ on: jobs: cherry-pick-commit: - uses: spring-io/spring-github-workflows/.github/workflows/spring-cherry-pick.yml@v3 + uses: spring-io/spring-github-workflows/.github/workflows/spring-cherry-pick.yml@v4 secrets: GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/backport-issue.yml b/.github/workflows/backport-issue.yml index 68de1977f1..b329f47c85 100644 --- a/.github/workflows/backport-issue.yml +++ b/.github/workflows/backport-issue.yml @@ -7,6 +7,6 @@ on: jobs: backport-issue: - uses: spring-io/spring-github-workflows/.github/workflows/spring-backport-issue.yml@v3 + uses: spring-io/spring-github-workflows/.github/workflows/spring-backport-issue.yml@v4 secrets: GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 5726382e2f..20e3d9067f 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -16,4 +16,4 @@ permissions: jobs: dispatch-docs-build: if: github.repository_owner == 'spring-projects' - uses: spring-io/spring-github-workflows/.github/workflows/spring-dispatch-docs-build.yml@v3 + uses: spring-io/spring-github-workflows/.github/workflows/spring-dispatch-docs-build.yml@v4 diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index b03b5bf93d..7f728da35c 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -8,4 +8,4 @@ on: jobs: build-pull-request: - uses: spring-io/spring-github-workflows/.github/workflows/spring-gradle-pull-request-build.yml@v3 + uses: spring-io/spring-github-workflows/.github/workflows/spring-gradle-pull-request-build.yml@v4 From 52c874ea460b6751b28abbd189eed844a16ce18c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 26 Oct 2024 02:57:34 +0000 Subject: [PATCH 600/737] Bump io.micrometer:micrometer-bom from 1.14.0-RC1 to 1.14.0-SNAPSHOT (#2884) Bumps [io.micrometer:micrometer-bom](https://github.com/micrometer-metrics/micrometer) from 1.14.0-RC1 to 1.14.0-SNAPSHOT. - [Release notes](https://github.com/micrometer-metrics/micrometer/releases) - [Commits](https://github.com/micrometer-metrics/micrometer/commits) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0d2197b5af..cb13f52e8a 100644 --- a/build.gradle +++ b/build.gradle @@ -60,7 +60,7 @@ ext { log4jVersion = '2.24.1' logbackVersion = '1.5.11' micrometerDocsVersion = '1.0.4' - micrometerVersion = '1.14.0-RC1' + micrometerVersion = '1.14.0-SNAPSHOT' micrometerTracingVersion = '1.4.0-RC1' mockitoVersion = '5.14.2' rabbitmqStreamVersion = '0.18.0' From 0d556305320c652f4a93991a7c98f5bfe8e168c2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 26 Oct 2024 02:58:23 +0000 Subject: [PATCH 601/737] Bump io.projectreactor:reactor-bom (#2878) Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2024.0.0-RC1 to 2024.0.0-SNAPSHOT. - [Release notes](https://github.com/reactor/reactor/releases) - [Commits](https://github.com/reactor/reactor/commits) --- updated-dependencies: - dependency-name: io.projectreactor:reactor-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index cb13f52e8a..0b20beefdb 100644 --- a/build.gradle +++ b/build.gradle @@ -65,7 +65,7 @@ ext { mockitoVersion = '5.14.2' rabbitmqStreamVersion = '0.18.0' rabbitmqVersion = '5.22.0' - reactorVersion = '2024.0.0-RC1' + reactorVersion = '2024.0.0-SNAPSHOT' springDataVersion = '2024.1.0-RC1' springRetryVersion = '2.0.10' springVersion = '6.2.0-RC2' From 6622c079cb7a3bad7d8e5e0f399f05e77cf85bb9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 26 Oct 2024 02:58:35 +0000 Subject: [PATCH 602/737] Bump ch.qos.logback:logback-classic from 1.5.11 to 1.5.12 (#2882) Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.11 to 1.5.12. - [Commits](https://github.com/qos-ch/logback/compare/v_1.5.11...v_1.5.12) --- updated-dependencies: - dependency-name: ch.qos.logback:logback-classic dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0b20beefdb..12453f2cea 100644 --- a/build.gradle +++ b/build.gradle @@ -58,7 +58,7 @@ ext { junitJupiterVersion = '5.11.3' kotlinCoroutinesVersion = '1.8.1' log4jVersion = '2.24.1' - logbackVersion = '1.5.11' + logbackVersion = '1.5.12' micrometerDocsVersion = '1.0.4' micrometerVersion = '1.14.0-SNAPSHOT' micrometerTracingVersion = '1.4.0-RC1' From 555a75cc13f21ce6c6bba6cadc14d484de515f28 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 26 Oct 2024 02:58:49 +0000 Subject: [PATCH 603/737] Bump org.testcontainers:testcontainers-bom from 1.20.2 to 1.20.3 (#2883) Bumps [org.testcontainers:testcontainers-bom](https://github.com/testcontainers/testcontainers-java) from 1.20.2 to 1.20.3. - [Release notes](https://github.com/testcontainers/testcontainers-java/releases) - [Changelog](https://github.com/testcontainers/testcontainers-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/testcontainers/testcontainers-java/compare/1.20.2...1.20.3) --- updated-dependencies: - dependency-name: org.testcontainers:testcontainers-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 12453f2cea..9213581551 100644 --- a/build.gradle +++ b/build.gradle @@ -69,7 +69,7 @@ ext { springDataVersion = '2024.1.0-RC1' springRetryVersion = '2.0.10' springVersion = '6.2.0-RC2' - testcontainersVersion = '1.20.2' + testcontainersVersion = '1.20.3' javaProjects = subprojects - project(':spring-amqp-bom') } From a7da5966765e0ae91cb30d3e336c3960c1685d1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 26 Oct 2024 03:00:21 +0000 Subject: [PATCH 604/737] Bump io.micrometer:micrometer-tracing-bom (#2879) Bumps [io.micrometer:micrometer-tracing-bom](https://github.com/micrometer-metrics/tracing) from 1.4.0-RC1 to 1.4.0-SNAPSHOT. - [Release notes](https://github.com/micrometer-metrics/tracing/releases) - [Commits](https://github.com/micrometer-metrics/tracing/commits) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-tracing-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 9213581551..1fb77fe485 100644 --- a/build.gradle +++ b/build.gradle @@ -61,7 +61,7 @@ ext { logbackVersion = '1.5.12' micrometerDocsVersion = '1.0.4' micrometerVersion = '1.14.0-SNAPSHOT' - micrometerTracingVersion = '1.4.0-RC1' + micrometerTracingVersion = '1.4.0-SNAPSHOT' mockitoVersion = '5.14.2' rabbitmqStreamVersion = '0.18.0' rabbitmqVersion = '5.22.0' From 88a1e6b5aef8567a4ed77d777cd2262c3bbdc901 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 26 Oct 2024 03:01:37 +0000 Subject: [PATCH 605/737] Bump org.springframework.data:spring-data-bom (#2881) Bumps [org.springframework.data:spring-data-bom](https://github.com/spring-projects/spring-data-bom) from 2024.1.0-RC1 to 2024.1.0-SNAPSHOT. - [Release notes](https://github.com/spring-projects/spring-data-bom/releases) - [Commits](https://github.com/spring-projects/spring-data-bom/commits) --- updated-dependencies: - dependency-name: org.springframework.data:spring-data-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 1fb77fe485..ccf0d841b6 100644 --- a/build.gradle +++ b/build.gradle @@ -66,7 +66,7 @@ ext { rabbitmqStreamVersion = '0.18.0' rabbitmqVersion = '5.22.0' reactorVersion = '2024.0.0-SNAPSHOT' - springDataVersion = '2024.1.0-RC1' + springDataVersion = '2024.1.0-SNAPSHOT' springRetryVersion = '2.0.10' springVersion = '6.2.0-RC2' testcontainersVersion = '1.20.3' From 1f0a6d3d7ec41da471b79fe921dfca1390baf369 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 26 Oct 2024 03:02:00 +0000 Subject: [PATCH 606/737] Bump org.springframework:spring-framework-bom (#2880) Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.2.0-RC2 to 6.2.0-SNAPSHOT. - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/commits) --- updated-dependencies: - dependency-name: org.springframework:spring-framework-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ccf0d841b6..096b2900aa 100644 --- a/build.gradle +++ b/build.gradle @@ -68,7 +68,7 @@ ext { reactorVersion = '2024.0.0-SNAPSHOT' springDataVersion = '2024.1.0-SNAPSHOT' springRetryVersion = '2.0.10' - springVersion = '6.2.0-RC2' + springVersion = '6.2.0-SNAPSHOT' testcontainersVersion = '1.20.3' javaProjects = subprojects - project(':spring-amqp-bom') From de60344b89cec4fec1a0976b0ff2cbc5cdfb81da Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 30 Oct 2024 15:41:26 -0400 Subject: [PATCH 607/737] Fix mentioning of the deprecated `ListenableFuture` * Some rearranging fixes for `nav.adoc` and `appendix\previous-whats-new` dir --- src/reference/antora/modules/ROOT/nav.adoc | 3 --- .../modules/ROOT/pages/amqp/request-reply.adoc | 2 -- .../previous-whats-new/changes-in-2-4-since-2-3.adoc | 12 ++++++++++++ .../previous-whats-new/earlier-releases.adoc | 6 ------ .../message-converter-changes.adoc | 7 ------- .../previous-whats-new/stream-support-changes.adoc | 7 ------- src/reference/antora/modules/ROOT/pages/stream.adoc | 2 -- 7 files changed, 12 insertions(+), 27 deletions(-) delete mode 100644 src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/earlier-releases.adoc delete mode 100644 src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/message-converter-changes.adoc delete mode 100644 src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/stream-support-changes.adoc diff --git a/src/reference/antora/modules/ROOT/nav.adoc b/src/reference/antora/modules/ROOT/nav.adoc index a947118935..b4327b384a 100644 --- a/src/reference/antora/modules/ROOT/nav.adoc +++ b/src/reference/antora/modules/ROOT/nav.adoc @@ -71,14 +71,11 @@ *** xref:appendix/previous-whats-new/changes-in-3-1-since-3-0.adoc[] *** xref:appendix/previous-whats-new/changes-in-3-0-since-2-4.adoc[] *** xref:appendix/previous-whats-new/changes-in-2-4-since-2-3.adoc[] -*** xref:appendix/previous-whats-new/message-converter-changes.adoc[] -*** xref:appendix/previous-whats-new/stream-support-changes.adoc[] *** xref:appendix/previous-whats-new/changes-in-2-3-since-2-2.adoc[] *** xref:appendix/previous-whats-new/changes-in-2-2-since-2-1.adoc[] *** xref:appendix/previous-whats-new/changes-in-2-1-since-2-0.adoc[] *** xref:appendix/previous-whats-new/changes-in-2-0-since-1-7.adoc[] *** xref:appendix/previous-whats-new/changes-in-1-7-since-1-6.adoc[] -*** xref:appendix/previous-whats-new/earlier-releases.adoc[] *** xref:appendix/previous-whats-new/changes-in-1-6-since-1-5.adoc[] *** xref:appendix/previous-whats-new/changes-in-1-5-since-1-4.adoc[] *** xref:appendix/previous-whats-new/changes-in-1-4-since-1-3.adoc[] diff --git a/src/reference/antora/modules/ROOT/pages/amqp/request-reply.adoc b/src/reference/antora/modules/ROOT/pages/amqp/request-reply.adoc index 2a17bb8350..3689437cb3 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/request-reply.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/request-reply.adoc @@ -290,8 +290,6 @@ Version 2.0 introduced variants of these methods (`convertSendAndReceiveAsType`) You must configure the underlying `RabbitTemplate` with a `SmartMessageConverter`. See xref:amqp/message-converters.adoc#json-complex[Converting From a `Message` With `RabbitTemplate`] for more information. -IMPORTANT: Starting with version 3.0, the `AsyncRabbitTemplate` methods now return `CompletableFuture` s instead of `ListenableFuture` s. - [[remoting]] == Spring Remoting with AMQP diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-4-since-2-3.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-4-since-2-3.adoc index 0dc4bf3fcc..372f46fcff 100644 --- a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-4-since-2-3.adoc +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-2-4-since-2-3.adoc @@ -22,3 +22,15 @@ See xref:amqp/broker-configuration.adoc#declarable-recovery[Recovering Auto-Dele Support remoting using Spring Framework’s RMI support is deprecated and will be removed in 3.0. See Spring Remoting with AMQP for more information. +[[stream-support-changes]] +== Stream Support Changes + +`RabbitStreamOperations` and `RabbitStreamTemplate` have been deprecated in favor of `RabbitStreamOperations2` and `RabbitStreamTemplate2` respectively; they return `CompletableFuture` instead of `ListenableFuture`. +See xref:stream.adoc[Using the RabbitMQ Stream Plugin] for more information. + +[[message-converter-changes]] +== Message Converter Changes + +The `Jackson2JsonMessageConverter` can now determine the charset from the `contentEncoding` header. +See xref:amqp/message-converters.adoc#json-message-converter[`Jackson2JsonMessageConverter`] for more information. + diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/earlier-releases.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/earlier-releases.adoc deleted file mode 100644 index d92f5b10f6..0000000000 --- a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/earlier-releases.adoc +++ /dev/null @@ -1,6 +0,0 @@ -[[earlier-releases]] -= Earlier Releases -:page-section-summary-toc: 1 - -See xref:appendix/previous-whats-new.adoc[Previous Releases] for changes in previous versions. - diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/message-converter-changes.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/message-converter-changes.adoc deleted file mode 100644 index 693a71cf40..0000000000 --- a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/message-converter-changes.adoc +++ /dev/null @@ -1,7 +0,0 @@ -[[message-converter-changes]] -= Message Converter Changes -:page-section-summary-toc: 1 - -The `Jackson2JsonMessageConverter` can now determine the charset from the `contentEncoding` header. -See xref:amqp/message-converters.adoc#json-message-converter[`Jackson2JsonMessageConverter`] for more information. - diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/stream-support-changes.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/stream-support-changes.adoc deleted file mode 100644 index b6be8ec44a..0000000000 --- a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/stream-support-changes.adoc +++ /dev/null @@ -1,7 +0,0 @@ -[[stream-support-changes]] -= Stream Support Changes -:page-section-summary-toc: 1 - -`RabbitStreamOperations` and `RabbitStreamTemplate` have been deprecated in favor of `RabbitStreamOperations2` and `RabbitStreamTemplate2` respectively; they return `CompletableFuture` instead of `ListenableFuture`. -See xref:stream.adoc[Using the RabbitMQ Stream Plugin] for more information. - diff --git a/src/reference/antora/modules/ROOT/pages/stream.adoc b/src/reference/antora/modules/ROOT/pages/stream.adoc index e9f5119e03..b1e35babbf 100644 --- a/src/reference/antora/modules/ROOT/pages/stream.adoc +++ b/src/reference/antora/modules/ROOT/pages/stream.adoc @@ -111,8 +111,6 @@ The `ProducerCustomizer` provides a mechanism to customize the producer before i Refer to the {rabbitmq-stream-docs}[Java Client Documentation] about customizing the `Environment` and `Producer`. -IMPORTANT: Starting with version 3.0, the method return types are `CompletableFuture` instead of `ListenableFuture`. - [[receiving-messages]] == Receiving Messages From 6daa286bc0e2e3eace8dc095dd7758999a49ba56 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Nov 2024 02:55:16 +0000 Subject: [PATCH 608/737] Bump com.fasterxml.jackson:jackson-bom from 2.18.0 to 2.18.1 (#2888) Bumps [com.fasterxml.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) from 2.18.0 to 2.18.1. - [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-2.18.0...jackson-bom-2.18.1) --- updated-dependencies: - dependency-name: com.fasterxml.jackson:jackson-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 096b2900aa..aa5190baa0 100644 --- a/build.gradle +++ b/build.gradle @@ -52,7 +52,7 @@ ext { commonsPoolVersion = '2.12.0' hamcrestVersion = '3.0' hibernateValidationVersion = '8.0.1.Final' - jacksonBomVersion = '2.18.0' + jacksonBomVersion = '2.18.1' jaywayJsonPathVersion = '2.9.0' junit4Version = '4.13.2' junitJupiterVersion = '5.11.3' From ccca9d9b8e5d2d2c2e6afe0c71353292363b77f7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Nov 2024 02:55:33 +0000 Subject: [PATCH 609/737] Bump the development-dependencies group with 2 updates (#2887) Bumps the development-dependencies group with 2 updates: [org.apache.httpcomponents.client5:httpclient5](https://github.com/apache/httpcomponents-client) and com.github.spotbugs. Updates `org.apache.httpcomponents.client5:httpclient5` from 5.4 to 5.4.1 - [Changelog](https://github.com/apache/httpcomponents-client/blob/rel/v5.4.1/RELEASE_NOTES.txt) - [Commits](https://github.com/apache/httpcomponents-client/compare/rel/v5.4...rel/v5.4.1) Updates `com.github.spotbugs` from 6.0.25 to 6.0.26 --- updated-dependencies: - dependency-name: org.apache.httpcomponents.client5:httpclient5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies - dependency-name: com.github.spotbugs dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index aa5190baa0..561bbbf04d 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ plugins { id 'io.spring.dependency-management' version '1.1.6' apply false id 'org.antora' version '1.0.0' id 'io.spring.antora.generate-antora-yml' version '0.0.1' - id 'com.github.spotbugs' version '6.0.25' + id 'com.github.spotbugs' version '6.0.26' id 'io.freefair.aggregate-javadoc' version '8.10.2' } @@ -48,7 +48,7 @@ ext { assertjVersion = '3.26.3' assertkVersion = '0.28.1' awaitilityVersion = '4.2.2' - commonsHttpClientVersion = '5.4' + commonsHttpClientVersion = '5.4.1' commonsPoolVersion = '2.12.0' hamcrestVersion = '3.0' hibernateValidationVersion = '8.0.1.Final' From 0c72649ec70afb4ac58001ee69cc3d02c44508ad Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Sat, 9 Nov 2024 05:02:14 +0700 Subject: [PATCH 610/737] Fix typos in Javadoc and variable names --- .../org/springframework/amqp/rabbit/junit/LongRunning.java | 4 ++-- .../rabbit/config/AbstractRabbitListenerContainerFactory.java | 4 ++-- .../springframework/amqp/rabbit/config/NamespaceUtils.java | 2 +- .../config/StatefulRetryOperationsInterceptorFactoryBean.java | 4 ++-- .../amqp/rabbit/connection/CompositeConnectionListener.java | 2 +- .../amqp/rabbit/connection/RabbitConnectionFactoryBean.java | 2 +- .../amqp/rabbit/listener/adapter/MessageListenerAdapter.java | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/LongRunning.java b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/LongRunning.java index f2e3a5d85c..9ea1221489 100644 --- a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/LongRunning.java +++ b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/LongRunning.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 the original author or authors. + * Copyright 2017-2024 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. @@ -40,7 +40,7 @@ public @interface LongRunning { /** - * The name of the variable/property used to determine whether long runnning tests + * The name of the variable/property used to determine whether long running tests * should run. * @return the name of the variable/property. */ diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java index f1ec63668a..bdbce98737 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2024 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. @@ -344,7 +344,7 @@ public void setObservationConvention(RabbitListenerObservationConvention observa /** * Set to true to stop the container after the current message(s) are processed and * requeue any prefetched. Useful when using exclusive or single-active consumers. - * @param forceStop true to stop when current messsage(s) are processed. + * @param forceStop true to stop when current message(s) are processed. * @since 2.4.15 */ public void setForceStop(boolean forceStop) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/NamespaceUtils.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/NamespaceUtils.java index d7b6dbd971..5b99acef0f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/NamespaceUtils.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/NamespaceUtils.java @@ -126,7 +126,7 @@ public static boolean addConstructorArgValueIfAttributeDefined(BeanDefinitionBui * @param builder the bean definition builder to be configured * @param element the XML element where the attribute should be defined * @param attributeName the name of the attribute whose value will be used as a constructor argument - * @param defaultValue the default value to use if the attirbute is not set + * @param defaultValue the default value to use if the attribute is not set */ public static void addConstructorArgBooleanValueIfAttributeDefined(BeanDefinitionBuilder builder, Element element, String attributeName, boolean defaultValue) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatefulRetryOperationsInterceptorFactoryBean.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatefulRetryOperationsInterceptorFactoryBean.java index 4a468f750f..4e79caaa98 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatefulRetryOperationsInterceptorFactoryBean.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatefulRetryOperationsInterceptorFactoryBean.java @@ -63,8 +63,8 @@ public class StatefulRetryOperationsInterceptorFactoryBean extends AbstractRetry private NewMessageIdentifier newMessageIdentifier; - public void setMessageKeyGenerator(MessageKeyGenerator messageKeyGeneretor) { - this.messageKeyGenerator = messageKeyGeneretor; + public void setMessageKeyGenerator(MessageKeyGenerator messageKeyGenerator) { + this.messageKeyGenerator = messageKeyGenerator; } public void setNewMessageIdentifier(NewMessageIdentifier newMessageIdentifier) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CompositeConnectionListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CompositeConnectionListener.java index 0c9900cd77..e8d9746446 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CompositeConnectionListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CompositeConnectionListener.java @@ -23,7 +23,7 @@ import com.rabbitmq.client.ShutdownSignalException; /** - * A composite listener that invokes its delegages in turn. + * A composite listener that invokes its delegates in turn. * * @author Dave Syer * @author Gary Russell diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitConnectionFactoryBean.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitConnectionFactoryBean.java index 779bdef7c9..944402231e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitConnectionFactoryBean.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitConnectionFactoryBean.java @@ -63,7 +63,7 @@ * optionally enabling SSL, with or without certificate validation. When * {@link #setSslPropertiesLocation(Resource) sslPropertiesLocation} is not null, the * default implementation loads a {@code PKCS12} keystore and a {@code JKS} truststore - * using the supplied properties and intializes key and trust manager factories, using + * using the supplied properties and initializes key and trust manager factories, using * algorithm {@code SunX509} by default. These are then used to initialize an * {@link SSLContext} using the {@link #setSslAlgorithm(String) sslAlgorithm} (default * TLSv1.2, falling back to TLSv1.1, if 1.2 is not available). diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapter.java index 65667d8453..b101a33954 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapter.java @@ -89,7 +89,7 @@ * * This next example illustrates a Message delegate that just consumes the String contents of * {@link Message Messages}. Notice also how the name of the Message handling method is different from the - * {@link #ORIGINAL_DEFAULT_LISTENER_METHOD original} (this will have to be configured in the attandant bean + * {@link #ORIGINAL_DEFAULT_LISTENER_METHOD original} (this will have to be configured in the attendant bean * definition). Again, no Message will be sent back as the method returns void. * *

From a2ac767c0d0462e1b8199015f4f5de12a4d8188e Mon Sep 17 00:00:00 2001
From: Artem Bilan 
Date: Mon, 11 Nov 2024 12:31:35 -0500
Subject: [PATCH 611/737] GH-2890: Fix `MessagingMessageListenerAdapter` for
 batch in Kotlin

Fixes: #2890
Issue link: https://github.com/spring-projects/spring-amqp/issues/2890

The Kotlin function with signature `receiveBatch(messages: List)`
produced a `WildCardType` for the generic of the `List` argument.

* Fix `MessagingMessageListenerAdapter` to use `TypeUtils.isAssignable()`
to determine if the `Type` has a part as expected type

**Auto-cherry-pick to `3.1.x`**
---
 .../adapter/MessagingMessageListenerAdapter.java   | 14 ++++++++------
 .../rabbit/annotation/EnableRabbitKotlinTests.kt   |  9 ++++++---
 2 files changed, 14 insertions(+), 9 deletions(-)

diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java
index 47ffbed9ee..88313d0bd2 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java
@@ -41,6 +41,7 @@
 import org.springframework.messaging.handler.annotation.Headers;
 import org.springframework.messaging.handler.annotation.Payload;
 import org.springframework.util.Assert;
+import org.springframework.util.TypeUtils;
 
 import com.rabbitmq.client.Channel;
 
@@ -456,17 +457,18 @@ private Type extractGenericParameterTypFromMethodParameter(MethodParameter metho
 				if (parameterizedType.getRawType().equals(Message.class)) {
 					genericParameterType = ((ParameterizedType) genericParameterType).getActualTypeArguments()[0];
 				}
-				else if (this.isBatch
-						&& ((parameterizedType.getRawType().equals(List.class)
-						|| parameterizedType.getRawType().equals(Collection.class))
-						&& parameterizedType.getActualTypeArguments().length == 1)) {
+				else if (this.isBatch &&
+						(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 pType
 							&& pType.getRawType().equals(Message.class);
-					this.isMessageList = paramType.equals(Message.class) || messageHasGeneric;
-					this.isAmqpMessageList = paramType.equals(org.springframework.amqp.core.Message.class);
+					this.isMessageList = TypeUtils.isAssignable(paramType, Message.class) || messageHasGeneric;
+					this.isAmqpMessageList =
+							TypeUtils.isAssignable(paramType, org.springframework.amqp.core.Message.class);
 					if (messageHasGeneric) {
 						genericParameterType = ((ParameterizedType) paramType).getActualTypeArguments()[0];
 					}
diff --git a/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt b/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt
index 4b94a657e6..8e9f452f9f 100644
--- a/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt
+++ b/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt
@@ -19,9 +19,11 @@ package org.springframework.amqp.rabbit.annotation
 import assertk.assertThat
 import assertk.assertions.containsOnly
 import assertk.assertions.isEqualTo
+import assertk.assertions.isInstanceOf
 import assertk.assertions.isTrue
 import org.junit.jupiter.api.Test
 import org.springframework.amqp.core.AcknowledgeMode
+import org.springframework.amqp.core.Message
 import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory
 import org.springframework.amqp.rabbit.connection.CachingConnectionFactory
 import org.springframework.amqp.rabbit.core.RabbitTemplate
@@ -77,7 +79,8 @@ class EnableRabbitKotlinTests {
 		template.convertAndSend("kotlinBatchQueue", "test1")
 		template.convertAndSend("kotlinBatchQueue", "test2")
 		assertThat(this.config.batchReceived.await(10, TimeUnit.SECONDS)).isTrue()
-		assertThat(this.config.batch).containsOnly("test1", "test2")
+		assertThat(this.config.batch[0]).isInstanceOf(Message::class.java)
+		assertThat(this.config.batch.map { m -> String(m.body) }).containsOnly("test1", "test2")
 	}
 
 	@Test
@@ -100,11 +103,11 @@ class EnableRabbitKotlinTests {
 
 		val batchReceived = CountDownLatch(1)
 
-		lateinit var batch: List
+		lateinit var batch: List
 
 		@RabbitListener(id = "batch", queues = ["kotlinBatchQueue"],
 				containerFactory = "batchRabbitListenerContainerFactory")
-		suspend fun receiveBatch(messages: List) {
+		suspend fun receiveBatch(messages: List) {
 			batch = messages
 			batchReceived.countDown()
 		}

From 547167960b9fa30fc50f7e0f9b74a880e69a739e Mon Sep 17 00:00:00 2001
From: Artem Bilan 
Date: Mon, 11 Nov 2024 18:15:42 -0500
Subject: [PATCH 612/737] GH-2891: Add
 `rabbitConnection.addShutdownListener(this)`

Fixes: #2891
Issue link: https://github.com/spring-projects/spring-amqp/issues/2891

The `ShutdownListener` is not registered into connections created by the `AbstractConnectionFactory`

* Fix `AbstractConnectionFactory.createBareConnection()` add itself into just created connection as a `ShutdownListener`
* Fix tests with mocks where `mockConnectionFactory.newConnection()` did not return an instance of `Connection`
---
 .../connection/AbstractConnectionFactory.java | 15 ++---
 .../AbstractConnectionFactoryTests.java       | 24 ++++----
 .../CachingConnectionFactoryTests.java        | 58 +++++++++++--------
 .../core/RabbitAdminDeclarationTests.java     | 45 --------------
 4 files changed, 48 insertions(+), 94 deletions(-)

diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java
index 33cb11dccf..97ef229871 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java
@@ -166,6 +166,7 @@ public void handleRecovery(Recoverable recoverable) {
 
 	@Nullable
 	private BackOff connectionCreatingBackOff;
+
 	/**
 	 * Create a new AbstractConnectionFactory for the given target ConnectionFactory, with no publisher connection
 	 * factory.
@@ -580,8 +581,8 @@ public ConnectionFactory getPublisherConnectionFactory() {
 	protected final Connection createBareConnection() {
 		try {
 			String connectionName = this.connectionNameStrategy.obtainNewConnectionName(this);
-
 			com.rabbitmq.client.Connection rabbitConnection = connect(connectionName);
+			rabbitConnection.addShutdownListener(this);
 			Connection connection = new SimpleConnection(rabbitConnection, this.closeTimeout,
 					this.connectionCreatingBackOff == null ? null : this.connectionCreatingBackOff.start());
 			if (rabbitConnection instanceof AutorecoveringConnection auto) {
@@ -732,16 +733,8 @@ public String toString() {
 		}
 	}
 
-	private static final class ConnectionBlockedListener implements BlockedListener {
-
-		private final Connection connection;
-
-		private final ApplicationEventPublisher applicationEventPublisher;
-
-		ConnectionBlockedListener(Connection connection, ApplicationEventPublisher applicationEventPublisher) {
-			this.connection = connection;
-			this.applicationEventPublisher = applicationEventPublisher;
-		}
+	private record ConnectionBlockedListener(Connection connection, ApplicationEventPublisher applicationEventPublisher)
+			implements BlockedListener {
 
 		@Override
 		public void handleBlocked(String reason) {
diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactoryTests.java
index 58546447c8..e41ea8635d 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactoryTests.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactoryTests.java
@@ -20,6 +20,7 @@
 import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyList;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.BDDMockito.given;
 import static org.mockito.BDDMockito.willCallRealMethod;
@@ -64,11 +65,11 @@ public abstract class AbstractConnectionFactoryTests {
 
 	@Test
 	public void testWithListener() throws Exception {
-
-		com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class);
-		com.rabbitmq.client.Connection mockConnection = mock(com.rabbitmq.client.Connection.class);
+		com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock();
+		com.rabbitmq.client.Connection mockConnection = mock();
 
 		given(mockConnectionFactory.newConnection(any(ExecutorService.class), anyString())).willReturn(mockConnection);
+		given(mockConnectionFactory.newConnection(any(), anyList(), anyString())).willReturn(mockConnection);
 
 		final AtomicInteger called = new AtomicInteger(0);
 		AbstractConnectionFactory connectionFactory = createConnectionFactory(mockConnectionFactory);
@@ -125,9 +126,8 @@ public void onClose(Connection connection) {
 
 	@Test
 	public void testWithListenerRegisteredAfterOpen() throws Exception {
-
-		com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class);
-		com.rabbitmq.client.Connection mockConnection = mock(com.rabbitmq.client.Connection.class);
+		com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock();
+		com.rabbitmq.client.Connection mockConnection = mock();
 
 		given(mockConnectionFactory.newConnection(any(ExecutorService.class), anyString())).willReturn(mockConnection);
 
@@ -168,10 +168,9 @@ public void onClose(Connection connection) {
 
 	@Test
 	public void testCloseInvalidConnection() throws Exception {
-
-		com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class);
-		com.rabbitmq.client.Connection mockConnection1 = mock(com.rabbitmq.client.Connection.class);
-		com.rabbitmq.client.Connection mockConnection2 = mock(com.rabbitmq.client.Connection.class);
+		com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock();
+		com.rabbitmq.client.Connection mockConnection1 = mock();
+		com.rabbitmq.client.Connection mockConnection2 = mock();
 
 		given(mockConnectionFactory.newConnection(any(ExecutorService.class), anyString()))
 				.willReturn(mockConnection1, mockConnection2);
@@ -194,8 +193,7 @@ public void testCloseInvalidConnection() throws Exception {
 
 	@Test
 	public void testDestroyBeforeUsed() throws Exception {
-
-		com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class);
+		com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock();
 
 		AbstractConnectionFactory connectionFactory = createConnectionFactory(mockConnectionFactory);
 		connectionFactory.destroy();
@@ -205,7 +203,7 @@ public void testDestroyBeforeUsed() throws Exception {
 
 	@Test
 	public void testCreatesConnectionWithGivenFactory() {
-		com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class);
+		com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock();
 		willCallRealMethod().given(mockConnectionFactory).params(any(ExecutorService.class));
 		willCallRealMethod().given(mockConnectionFactory).setThreadFactory(any(ThreadFactory.class));
 		willCallRealMethod().given(mockConnectionFactory).getThreadFactory();
diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java
index 66dc8013ec..8ecddfcb40 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java
@@ -22,6 +22,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyList;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.argThat;
@@ -117,7 +118,7 @@ void stringRepresentation() {
 		assertThat(ccf.toString()).contains(", addresses=[h3:1236, h4:1237]")
 				.doesNotContain("host")
 				.doesNotContain("port");
-		ccf.setAddressResolver(() ->  {
+		ccf.setAddressResolver(() -> {
 			throw new IOException("test");
 		});
 		ccf.setPort(0);
@@ -710,7 +711,7 @@ public void testCheckoutLimitWithPublisherConfirmsLogicalAlreadyCloses() throws
 		willAnswer(invoc -> {
 			open.set(false); // so the logical close detects a closed delegate
 			return null;
-		}).given(mockChannel).basicPublish(any(), any(), anyBoolean(),  any(), any());
+		}).given(mockChannel).basicPublish(any(), any(), anyBoolean(), any(), any());
 
 		CachingConnectionFactory ccf = new CachingConnectionFactory(mockConnectionFactory);
 		ccf.setExecutor(mock(ExecutorService.class));
@@ -722,7 +723,7 @@ public void testCheckoutLimitWithPublisherConfirmsLogicalAlreadyCloses() throws
 		rabbitTemplate.convertAndSend("foo", "bar");
 		open.set(true);
 		rabbitTemplate.convertAndSend("foo", "bar");
-		verify(mockChannel, times(2)).basicPublish(any(), any(), anyBoolean(),  any(), any());
+		verify(mockChannel, times(2)).basicPublish(any(), any(), anyBoolean(), any(), any());
 	}
 
 	@Test
@@ -1300,7 +1301,6 @@ public void onClose(Connection connection) {
 		verify(mockConnections.get(3)).close(30000);
 	}
 
-
 	@Test
 	public void testWithConnectionFactoryCachedConnectionAndChannels() throws Exception {
 		com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class);
@@ -1644,6 +1644,8 @@ private void verifyChannelIs(Channel mockChannel, Channel channel) {
 	@Test
 	public void setAddressesEmpty() throws Exception {
 		ConnectionFactory mock = mock(com.rabbitmq.client.ConnectionFactory.class);
+		given(mock.newConnection(any(ExecutorService.class), anyString()))
+				.willReturn(mock(com.rabbitmq.client.Connection.class));
 		CachingConnectionFactory ccf = new CachingConnectionFactory(mock);
 		ccf.setExecutor(mock(ExecutorService.class));
 		ccf.setHost("abc");
@@ -1663,6 +1665,8 @@ public void setAddressesEmpty() throws Exception {
 	@Test
 	public void setAddressesOneHost() throws Exception {
 		ConnectionFactory mock = mock(com.rabbitmq.client.ConnectionFactory.class);
+		given(mock.newConnection(any(), anyList(), anyString()))
+				.willReturn(mock(com.rabbitmq.client.Connection.class));
 		CachingConnectionFactory ccf = new CachingConnectionFactory(mock);
 		ccf.setAddresses("mq1");
 		ccf.createConnection();
@@ -1674,8 +1678,9 @@ public void setAddressesOneHost() throws Exception {
 
 	@Test
 	public void setAddressesTwoHosts() throws Exception {
-		ConnectionFactory mock = mock(com.rabbitmq.client.ConnectionFactory.class);
+		ConnectionFactory mock = mock();
 		willReturn(true).given(mock).isAutomaticRecoveryEnabled();
+		willReturn(mock(com.rabbitmq.client.Connection.class)).given(mock).newConnection(any(), anyList(), anyString());
 		CachingConnectionFactory ccf = new CachingConnectionFactory(mock);
 		ccf.setAddresses("mq1,mq2");
 		ccf.createConnection();
@@ -1683,7 +1688,8 @@ public void setAddressesTwoHosts() throws Exception {
 		verify(mock).setAutomaticRecoveryEnabled(false);
 		verify(mock).newConnection(
 				isNull(),
-				argThat((ArgumentMatcher>) a -> a.size() == 2 && a.contains(new Address("mq1")) && a.contains(new Address("mq2"))),
+				argThat((ArgumentMatcher>) a -> a.size() == 2
+						&& a.contains(new Address("mq1")) && a.contains(new Address("mq2"))),
 				anyString());
 		verifyNoMoreInteractions(mock);
 	}
@@ -1692,7 +1698,9 @@ public void setAddressesTwoHosts() throws Exception {
 	public void setUri() throws Exception {
 		URI uri = new URI("amqp://localhost:1234/%2f");
 
-		ConnectionFactory mock = mock(com.rabbitmq.client.ConnectionFactory.class);
+		ConnectionFactory mock = mock();
+		given(mock.newConnection(any(ExecutorService.class), anyString()))
+				.willReturn(mock(com.rabbitmq.client.Connection.class));
 		CachingConnectionFactory ccf = new CachingConnectionFactory(mock);
 		ccf.setExecutor(mock(ExecutorService.class));
 
@@ -1854,12 +1862,12 @@ public void testFirstConnectionDoesntWait() throws IOException, TimeoutException
 	@SuppressWarnings("unchecked")
 	@Test
 	public void testShuffleRandom() throws IOException, TimeoutException {
-		com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class);
-		com.rabbitmq.client.Connection mockConnection = mock(com.rabbitmq.client.Connection.class);
+		com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock();
+		com.rabbitmq.client.Connection mockConnection = mock();
 		Channel mockChannel = mock(Channel.class);
 
-		given(mockConnectionFactory.newConnection((ExecutorService) isNull(), any(List.class), anyString()))
-			.willReturn(mockConnection);
+		given(mockConnectionFactory.newConnection(any(), anyList(), anyString()))
+				.willReturn(mockConnection);
 		given(mockConnection.createChannel()).willReturn(mockChannel);
 		given(mockChannel.isOpen()).willReturn(true);
 		given(mockConnection.isOpen()).willReturn(true);
@@ -1873,11 +1881,11 @@ public void testShuffleRandom() throws IOException, TimeoutException {
 		ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class);
 		verify(mockConnectionFactory, times(100)).newConnection(isNull(), captor.capture(), anyString());
 		List firstAddress = captor.getAllValues()
-			.stream()
-			.map(addresses -> addresses.get(0).getHost())
-			.distinct()
-			.sorted()
-			.collect(Collectors.toList());
+				.stream()
+				.map(addresses -> addresses.get(0).getHost())
+				.distinct()
+				.sorted()
+				.collect(Collectors.toList());
 		assertThat(firstAddress).containsExactly("host1", "host2", "host3");
 	}
 
@@ -1888,8 +1896,8 @@ public void testShuffleInOrder() throws IOException, TimeoutException {
 		com.rabbitmq.client.Connection mockConnection = mock(com.rabbitmq.client.Connection.class);
 		Channel mockChannel = mock(Channel.class);
 
-		given(mockConnectionFactory.newConnection((ExecutorService) isNull(), any(List.class), anyString()))
-			.willReturn(mockConnection);
+		given(mockConnectionFactory.newConnection(isNull(), anyList(), anyString()))
+				.willReturn(mockConnection);
 		given(mockConnection.createChannel()).willReturn(mockChannel);
 		given(mockChannel.isOpen()).willReturn(true);
 		given(mockConnection.isOpen()).willReturn(true);
@@ -1903,17 +1911,17 @@ public void testShuffleInOrder() throws IOException, TimeoutException {
 		ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class);
 		verify(mockConnectionFactory, times(3)).newConnection(isNull(), captor.capture(), anyString());
 		List connectAddresses = captor.getAllValues()
-			.stream()
-			.map(addresses -> addresses.get(0).getHost())
-			.collect(Collectors.toList());
+				.stream()
+				.map(addresses -> addresses.get(0).getHost())
+				.collect(Collectors.toList());
 		assertThat(connectAddresses).containsExactly("host1", "host2", "host3");
 	}
 
 	@Test
 	void testResolver() throws Exception {
-		com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class);
-		com.rabbitmq.client.Connection mockConnection = mock(com.rabbitmq.client.Connection.class);
-		Channel mockChannel = mock(Channel.class);
+		com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock();
+		com.rabbitmq.client.Connection mockConnection = mock();
+		Channel mockChannel = mock();
 
 		AddressResolver resolver = () -> Collections.singletonList(Address.parseAddress("foo:5672"));
 		given(mockConnectionFactory.newConnection(any(ExecutorService.class), eq(resolver), anyString()))
@@ -1934,7 +1942,7 @@ void testResolver() throws Exception {
 
 	@Test
 	void nullShutdownCause() {
-		com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class);
+		com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock();
 		AbstractConnectionFactory cf = createConnectionFactory(mockConnectionFactory);
 		AtomicBoolean connShutDown = new AtomicBoolean();
 		cf.addConnectionListener(new ConnectionListener() {
diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminDeclarationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminDeclarationTests.java
index fe9b092554..a6d04075b3 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminDeclarationTests.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminDeclarationTests.java
@@ -32,12 +32,9 @@
 import static org.mockito.Mockito.verify;
 
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 
 import org.junit.jupiter.api.Test;
@@ -50,7 +47,6 @@
 import org.springframework.amqp.core.Exchange;
 import org.springframework.amqp.core.Queue;
 import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
-import org.springframework.amqp.rabbit.connection.CachingConnectionFactory.CacheMode;
 import org.springframework.amqp.rabbit.connection.Connection;
 import org.springframework.amqp.rabbit.connection.ConnectionFactory;
 import org.springframework.amqp.rabbit.connection.ConnectionListener;
@@ -104,47 +100,6 @@ public void testUnconditional() throws Exception {
 		verify(channel).queueBind("foo", "bar", "foo", new HashMap<>());
 	}
 
-	@Test
-	public void testNoDeclareWithCachedConnections() throws Exception {
-		com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class);
-
-		List mockChannels = new ArrayList<>();
-
-		AtomicInteger connectionNumber = new AtomicInteger();
-		willAnswer(invocation -> {
-			com.rabbitmq.client.Connection connection = mock(com.rabbitmq.client.Connection.class);
-			AtomicInteger channelNumber = new AtomicInteger();
-			willAnswer(invocation1 -> {
-				Channel channel = mock(Channel.class);
-				given(channel.isOpen()).willReturn(true);
-				int channelNum = channelNumber.incrementAndGet();
-				given(channel.toString()).willReturn("mockChannel" + channelNum);
-				mockChannels.add(channel);
-				return channel;
-			}).given(connection).createChannel();
-			int connectionNum = connectionNumber.incrementAndGet();
-			given(connection.toString()).willReturn("mockConnection" + connectionNum);
-			given(connection.isOpen()).willReturn(true);
-			return connection;
-		}).given(mockConnectionFactory).newConnection((ExecutorService) null);
-
-		CachingConnectionFactory ccf = new CachingConnectionFactory(mockConnectionFactory);
-		ccf.setCacheMode(CacheMode.CONNECTION);
-		ccf.afterPropertiesSet();
-
-		RabbitAdmin admin = new RabbitAdmin(ccf);
-		GenericApplicationContext context = new GenericApplicationContext();
-		Queue queue = new Queue("foo");
-		context.getBeanFactory().registerSingleton("foo", queue);
-		context.refresh();
-		admin.setApplicationContext(context);
-		admin.afterPropertiesSet();
-		ccf.createConnection().close();
-		ccf.destroy();
-
-		assertThat(mockChannels.size()).as("Admin should not have created a channel").isEqualTo(0);
-	}
-
 	@Test
 	public void testUnconditionalWithExplicitFactory() throws Exception {
 		ConnectionFactory cf = mock(ConnectionFactory.class);

From f112115ead608ca4d9a7aceb6d3d9fa536704ede Mon Sep 17 00:00:00 2001
From: Alex Dumitrescu <6016800+axldev@users.noreply.github.com>
Date: Tue, 12 Nov 2024 16:16:19 +0100
Subject: [PATCH 613/737] Fix typos in Javadoc

---
 .../amqp/support/converter/MessageConversionException.java      | 2 +-
 .../springframework/amqp/rabbit/annotation/EnableRabbit.java    | 2 +-
 .../amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java    | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessageConversionException.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessageConversionException.java
index 376a3c2429..c2e879394d 100644
--- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessageConversionException.java
+++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessageConversionException.java
@@ -23,7 +23,7 @@
  * Exception to be thrown by message converters if they encounter a problem with converting a message or object.
  * 

*

- * N.B. this is not an {@link AmqpException} because it is a a client exception, not a protocol or broker + * N.B. this is not an {@link AmqpException} because it is a client exception, not a protocol or broker * problem. *

* diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/EnableRabbit.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/EnableRabbit.java index b22cc737d9..a83d043419 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/EnableRabbit.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/EnableRabbit.java @@ -104,7 +104,7 @@ *

Annotated methods can use flexible signature; in particular, it is possible to use * the {@link org.springframework.messaging.Message Message} abstraction and related annotations, * see {@link RabbitListener} Javadoc for more details. For instance, the following would - * inject the content of the message and a a custom "myCounter" AMQP header: + * inject the content of the message and a custom "myCounter" AMQP header: * *

  * @RabbitListener(containerFactory = "myRabbitListenerContainerFactory", queues = "myQueue")
diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java
index de0a38f538..95bcaffc40 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java
@@ -426,7 +426,7 @@ public void setupListenerContainer(MessageListenerContainer listenerContainer) {
 	 * Create a {@link MessageListener} that is able to serve this endpoint for the
 	 * specified container.
 	 * @param container the {@link MessageListenerContainer} to create a {@link MessageListener}.
-	 * @return a a {@link MessageListener} instance.
+	 * @return a {@link MessageListener} instance.
 	 */
 	protected abstract MessageListener createMessageListener(MessageListenerContainer container);
 

From 9eb857c1cd693880aced2d0f2fe8415d4f3680b0 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 16 Nov 2024 02:15:28 +0000
Subject: [PATCH 614/737] Bump io.micrometer:micrometer-bom (#2901)

Bumps [io.micrometer:micrometer-bom](https://github.com/micrometer-metrics/micrometer) from 1.14.0-SNAPSHOT to 1.14.2-SNAPSHOT.
- [Release notes](https://github.com/micrometer-metrics/micrometer/releases)
- [Commits](https://github.com/micrometer-metrics/micrometer/commits)

---
updated-dependencies:
- dependency-name: io.micrometer:micrometer-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index 561bbbf04d..413062e569 100644
--- a/build.gradle
+++ b/build.gradle
@@ -60,7 +60,7 @@ ext {
 	log4jVersion = '2.24.1'
 	logbackVersion = '1.5.12'
 	micrometerDocsVersion = '1.0.4'
-	micrometerVersion = '1.14.0-SNAPSHOT'
+	micrometerVersion = '1.14.2-SNAPSHOT'
 	micrometerTracingVersion = '1.4.0-SNAPSHOT'
 	mockitoVersion = '5.14.2'
 	rabbitmqStreamVersion = '0.18.0'

From 282fef8ae220ac36aeba17543ff18c2128276376 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 16 Nov 2024 02:15:58 +0000
Subject: [PATCH 615/737] Bump io.projectreactor:reactor-bom (#2897)

Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2024.0.0-SNAPSHOT to 2024.0.1-SNAPSHOT.
- [Release notes](https://github.com/reactor/reactor/releases)
- [Commits](https://github.com/reactor/reactor/commits)

---
updated-dependencies:
- dependency-name: io.projectreactor:reactor-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index 413062e569..dd8935fec3 100644
--- a/build.gradle
+++ b/build.gradle
@@ -65,7 +65,7 @@ ext {
 	mockitoVersion = '5.14.2'
 	rabbitmqStreamVersion = '0.18.0'
 	rabbitmqVersion = '5.22.0'
-	reactorVersion = '2024.0.0-SNAPSHOT'
+	reactorVersion = '2024.0.1-SNAPSHOT'
 	springDataVersion = '2024.1.0-SNAPSHOT'
 	springRetryVersion = '2.0.10'
 	springVersion = '6.2.0-SNAPSHOT'

From 287a5f9d04bf27860309f946fc5fa210e749d5c4 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 16 Nov 2024 02:16:09 +0000
Subject: [PATCH 616/737] Bump org.springframework:spring-framework-bom (#2899)

Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.2.0-SNAPSHOT to 6.2.1-SNAPSHOT.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/commits)

---
updated-dependencies:
- dependency-name: org.springframework:spring-framework-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index dd8935fec3..caf4f204bc 100644
--- a/build.gradle
+++ b/build.gradle
@@ -68,7 +68,7 @@ ext {
 	reactorVersion = '2024.0.1-SNAPSHOT'
 	springDataVersion = '2024.1.0-SNAPSHOT'
 	springRetryVersion = '2.0.10'
-	springVersion = '6.2.0-SNAPSHOT'
+	springVersion = '6.2.1-SNAPSHOT'
 	testcontainersVersion = '1.20.3'
 
 	javaProjects = subprojects - project(':spring-amqp-bom')

From ac875c893b7882b52f9ab86ef612ed14f8f55f46 Mon Sep 17 00:00:00 2001
From: Tran Ngoc Nhan 
Date: Mon, 18 Nov 2024 21:52:02 +0700
Subject: [PATCH 617/737] Fix typo in reference document

---
 .../antora/modules/ROOT/pages/amqp/containerAttributes.adoc     | 2 +-
 .../async-annotation-driven/error-handling.adoc                 | 2 +-
 src/reference/antora/modules/ROOT/pages/amqp/template.adoc      | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/reference/antora/modules/ROOT/pages/amqp/containerAttributes.adoc b/src/reference/antora/modules/ROOT/pages/amqp/containerAttributes.adoc
index f876898405..f3172bf9fe 100644
--- a/src/reference/antora/modules/ROOT/pages/amqp/containerAttributes.adoc
+++ b/src/reference/antora/modules/ROOT/pages/amqp/containerAttributes.adoc
@@ -435,7 +435,7 @@ a|
 (mismatched-queues-fatal)
 
 a|When the container starts, if this property is `true` (default: `false`), the container checks that all queues declared in the context are compatible with queues already on the broker.
-If mismatched properties (such as `auto-delete`) or arguments (skuch as `x-message-ttl`) exist, the container (and application context) fails to start with a fatal exception.
+If mismatched properties (such as `auto-delete`) or arguments (such as `x-message-ttl`) exist, the container (and application context) fails to start with a fatal exception.
 
 If the problem is detected during recovery (for example, after a lost connection), the container is stopped.
 
diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/error-handling.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/error-handling.adoc
index 9c26c69de0..fe273d6adf 100644
--- a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/error-handling.adoc
+++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/error-handling.adoc
@@ -36,7 +36,7 @@ If you use JSON, consider using an `errorHandler` to return some other Jackson-f
 
 IMPORTANT: In version 2.1, this interface moved from package `o.s.amqp.rabbit.listener` to `o.s.amqp.rabbit.listener.api`.
 
-Starting with version 2.1.7, the `Channel` is available in a messaging message header; this allows you to ack or nack the failed messasge when using `AcknowledgeMode.MANUAL`:
+Starting with version 2.1.7, the `Channel` is available in a messaging message header; this allows you to ack or nack the failed message when using `AcknowledgeMode.MANUAL`:
 
 [source, java]
 ----
diff --git a/src/reference/antora/modules/ROOT/pages/amqp/template.adoc b/src/reference/antora/modules/ROOT/pages/amqp/template.adoc
index 55b17e8cb3..0dea17c5d3 100644
--- a/src/reference/antora/modules/ROOT/pages/amqp/template.adoc
+++ b/src/reference/antora/modules/ROOT/pages/amqp/template.adoc
@@ -195,7 +195,7 @@ In general, this means that only one confirm is outstanding on a channel at a ti
 NOTE: Starting with version 2.2, the callbacks are invoked on one of the connection factory's `executor` threads.
 This is to avoid a potential deadlock if you perform Rabbit operations from within the callback.
 With previous versions, the callbacks were invoked directly on the `amqp-client` connection I/O thread; this would deadlock if you perform some RPC operation (such as opening a new channel) since the I/O thread blocks waiting for the result, but the result needs to be processed by the I/O thread itself.
-With those versions, it was necessary to hand off work (such as sending a messasge) to another thread within the callback.
+With those versions, it was necessary to hand off work (such as sending a message) to another thread within the callback.
 This is no longer necessary since the framework now hands off the callback invocation to the executor.
 
 IMPORTANT: The guarantee of receiving a returned message before the ack is still maintained as long as the return callback executes in 60 seconds or less.

From 03258365869583dd4f15d63ba125bd4b98dc880b Mon Sep 17 00:00:00 2001
From: Artem Bilan 
Date: Mon, 18 Nov 2024 12:41:38 -0500
Subject: [PATCH 618/737] Revert commits for Dependabot updates to SNAPSHOTs

Essentially, Dependabot must update to the latest GA, but not SNAPSHOT

Revert "Bump org.springframework:spring-framework-bom (#2899)"

This reverts commit 287a5f9d04bf27860309f946fc5fa210e749d5c4.

Revert "Bump io.projectreactor:reactor-bom (#2897)"

This reverts commit 282fef8ae220ac36aeba17543ff18c2128276376.

Revert "Bump io.micrometer:micrometer-bom (#2901)"

This reverts commit 9eb857c1cd693880aced2d0f2fe8415d4f3680b0.
---
 build.gradle | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/build.gradle b/build.gradle
index caf4f204bc..561bbbf04d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -60,15 +60,15 @@ ext {
 	log4jVersion = '2.24.1'
 	logbackVersion = '1.5.12'
 	micrometerDocsVersion = '1.0.4'
-	micrometerVersion = '1.14.2-SNAPSHOT'
+	micrometerVersion = '1.14.0-SNAPSHOT'
 	micrometerTracingVersion = '1.4.0-SNAPSHOT'
 	mockitoVersion = '5.14.2'
 	rabbitmqStreamVersion = '0.18.0'
 	rabbitmqVersion = '5.22.0'
-	reactorVersion = '2024.0.1-SNAPSHOT'
+	reactorVersion = '2024.0.0-SNAPSHOT'
 	springDataVersion = '2024.1.0-SNAPSHOT'
 	springRetryVersion = '2.0.10'
-	springVersion = '6.2.1-SNAPSHOT'
+	springVersion = '6.2.0-SNAPSHOT'
 	testcontainersVersion = '1.20.3'
 
 	javaProjects = subprojects - project(':spring-amqp-bom')

From d910a8b2bf62bc5212ca93636a85d95ac041c28f Mon Sep 17 00:00:00 2001
From: Artem Bilan 
Date: Mon, 18 Nov 2024 12:50:56 -0500
Subject: [PATCH 619/737] Upgrade Spring deps to GAs; prepare for release

---
 build.gradle | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/build.gradle b/build.gradle
index 561bbbf04d..f167ef1ac5 100644
--- a/build.gradle
+++ b/build.gradle
@@ -60,15 +60,15 @@ ext {
 	log4jVersion = '2.24.1'
 	logbackVersion = '1.5.12'
 	micrometerDocsVersion = '1.0.4'
-	micrometerVersion = '1.14.0-SNAPSHOT'
-	micrometerTracingVersion = '1.4.0-SNAPSHOT'
+	micrometerVersion = '1.14.1'
+	micrometerTracingVersion = '1.4.0'
 	mockitoVersion = '5.14.2'
 	rabbitmqStreamVersion = '0.18.0'
 	rabbitmqVersion = '5.22.0'
-	reactorVersion = '2024.0.0-SNAPSHOT'
-	springDataVersion = '2024.1.0-SNAPSHOT'
+	reactorVersion = '2024.0.0'
+	springDataVersion = '2024.1.0'
 	springRetryVersion = '2.0.10'
-	springVersion = '6.2.0-SNAPSHOT'
+	springVersion = '6.2.0'
 	testcontainersVersion = '1.20.3'
 
 	javaProjects = subprojects - project(':spring-amqp-bom')

From 562bc772c436e6b09527084f238b46588ba8ba6d Mon Sep 17 00:00:00 2001
From: Artem Bilan 
Date: Mon, 18 Nov 2024 14:49:30 -0500
Subject: [PATCH 620/737] GH-2907: Use `CF.closeTimeout` for confirms wait

Fixes: #2907

Issue link: https://github.com/spring-projects/spring-amqp/issues/2907

The current hard-coded `5 seconds` is not enough in real applications under heavy load

* Fix `CachingConnectionFactory` to use `getCloseTimeout()` for `publisherCallbackChannel.waitForConfirms()`
which is `30 seconds` by default, but can be modified via `CachingConnectionFactory.setCloseTimeout()`

**Auto-cherry-pick to `3.1.x`**
---
 .../amqp/rabbit/connection/AbstractConnectionFactory.java | 5 +++--
 .../amqp/rabbit/connection/CachingConnectionFactory.java  | 8 +++-----
 2 files changed, 6 insertions(+), 7 deletions(-)

diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java
index 97ef229871..caa57d6c92 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java
@@ -479,8 +479,9 @@ protected ExecutorService getExecutorService() {
 	}
 
 	/**
-	 * How long to wait (milliseconds) for a response to a connection close operation from the broker; default 30000 (30
-	 * seconds).
+	 * How long to wait (milliseconds) for a response to a connection close operation from the broker;
+	 * default 30000 (30 seconds).
+	 * Also used for {@link com.rabbitmq.client.Channel#waitForConfirms()}.
 	 * @param closeTimeout the closeTimeout to set.
 	 */
 	public void setCloseTimeout(int closeTimeout) {
diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java
index f14aa989b9..0aade48321 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java
@@ -1087,8 +1087,6 @@ public String toString() {
 
 	private final class CachedChannelInvocationHandler implements InvocationHandler {
 
-		private static final int ASYNC_CLOSE_TIMEOUT = 5_000;
-
 		private final ChannelCachingConnectionProxy theConnection;
 
 		private final Deque channelList;
@@ -1302,7 +1300,7 @@ private void returnToCache(ChannelProxy proxy) {
 					getChannelsExecutor()
 							.execute(() -> {
 								try {
-									publisherCallbackChannel.waitForConfirms(ASYNC_CLOSE_TIMEOUT);
+									publisherCallbackChannel.waitForConfirms(getCloseTimeout());
 								}
 								catch (InterruptedException ex) {
 									Thread.currentThread().interrupt();
@@ -1426,10 +1424,10 @@ private void asyncClose() {
 				executorService.execute(() -> {
 					try {
 						if (ConfirmType.CORRELATED.equals(CachingConnectionFactory.this.confirmType)) {
-							channel.waitForConfirmsOrDie(ASYNC_CLOSE_TIMEOUT);
+							channel.waitForConfirmsOrDie(getCloseTimeout());
 						}
 						else {
-							Thread.sleep(ASYNC_CLOSE_TIMEOUT);
+							Thread.sleep(5_000); // NOSONAR - some time to give the channel a chance to ack
 						}
 					}
 					catch (@SuppressWarnings(UNUSED) InterruptedException e1) {

From da7da60f40c55056c20487a3b59d6f70e4ea13e4 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 18 Nov 2024 20:59:27 +0000
Subject: [PATCH 621/737] Bump spring-io/spring-github-workflows (#2910)

Bumps the development-dependencies group with 1 update: [spring-io/spring-github-workflows](https://github.com/spring-io/spring-github-workflows).


Updates `spring-io/spring-github-workflows` from 4 to 5
- [Release notes](https://github.com/spring-io/spring-github-workflows/releases)
- [Commits](https://github.com/spring-io/spring-github-workflows/compare/v4...v5)

---
updated-dependencies:
- dependency-name: spring-io/spring-github-workflows
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/workflows/announce-milestone-planning.yml | 2 +-
 .github/workflows/auto-cherry-pick.yml            | 2 +-
 .github/workflows/backport-issue.yml              | 2 +-
 .github/workflows/deploy-docs.yml                 | 2 +-
 .github/workflows/merge-dependabot-pr.yml         | 2 +-
 .github/workflows/pr-build.yml                    | 2 +-
 .github/workflows/release.yml                     | 2 +-
 7 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/.github/workflows/announce-milestone-planning.yml b/.github/workflows/announce-milestone-planning.yml
index 1075304cd1..58ba601906 100644
--- a/.github/workflows/announce-milestone-planning.yml
+++ b/.github/workflows/announce-milestone-planning.yml
@@ -6,6 +6,6 @@ on:
 
 jobs:
   announce-milestone-planning:
-    uses: spring-io/spring-github-workflows/.github/workflows/spring-announce-milestone-planning.yml@v4
+    uses: spring-io/spring-github-workflows/.github/workflows/spring-announce-milestone-planning.yml@v5
     secrets:
       SPRING_RELEASE_CHAT_WEBHOOK_URL: ${{ secrets.SPRING_RELEASE_GCHAT_WEBHOOK_URL }}
\ No newline at end of file
diff --git a/.github/workflows/auto-cherry-pick.yml b/.github/workflows/auto-cherry-pick.yml
index bb476cb186..6ba14dde29 100644
--- a/.github/workflows/auto-cherry-pick.yml
+++ b/.github/workflows/auto-cherry-pick.yml
@@ -8,6 +8,6 @@ on:
 
 jobs:
   cherry-pick-commit:
-    uses: spring-io/spring-github-workflows/.github/workflows/spring-cherry-pick.yml@v4
+    uses: spring-io/spring-github-workflows/.github/workflows/spring-cherry-pick.yml@v5
     secrets:
       GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
\ No newline at end of file
diff --git a/.github/workflows/backport-issue.yml b/.github/workflows/backport-issue.yml
index b329f47c85..71e42771d5 100644
--- a/.github/workflows/backport-issue.yml
+++ b/.github/workflows/backport-issue.yml
@@ -7,6 +7,6 @@ on:
 
 jobs:
   backport-issue:
-    uses: spring-io/spring-github-workflows/.github/workflows/spring-backport-issue.yml@v4
+    uses: spring-io/spring-github-workflows/.github/workflows/spring-backport-issue.yml@v5
     secrets:
       GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
\ No newline at end of file
diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml
index 20e3d9067f..2065ee7187 100644
--- a/.github/workflows/deploy-docs.yml
+++ b/.github/workflows/deploy-docs.yml
@@ -16,4 +16,4 @@ permissions:
 jobs:
   dispatch-docs-build:
     if: github.repository_owner == 'spring-projects'
-    uses: spring-io/spring-github-workflows/.github/workflows/spring-dispatch-docs-build.yml@v4
+    uses: spring-io/spring-github-workflows/.github/workflows/spring-dispatch-docs-build.yml@v5
diff --git a/.github/workflows/merge-dependabot-pr.yml b/.github/workflows/merge-dependabot-pr.yml
index 44a74e507b..f513c72567 100644
--- a/.github/workflows/merge-dependabot-pr.yml
+++ b/.github/workflows/merge-dependabot-pr.yml
@@ -12,7 +12,7 @@ jobs:
   merge-dependabot-pr:
     permissions: write-all
 
-    uses: spring-io/spring-github-workflows/.github/workflows/spring-merge-dependabot-pr.yml@v4
+    uses: spring-io/spring-github-workflows/.github/workflows/spring-merge-dependabot-pr.yml@v5
     with:
       mergeArguments: --auto --squash
       autoMergeSnapshots: true
\ No newline at end of file
diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml
index 7f728da35c..c9f71e8bdf 100644
--- a/.github/workflows/pr-build.yml
+++ b/.github/workflows/pr-build.yml
@@ -8,4 +8,4 @@ on:
 
 jobs:
   build-pull-request:
-    uses: spring-io/spring-github-workflows/.github/workflows/spring-gradle-pull-request-build.yml@v4
+    uses: spring-io/spring-github-workflows/.github/workflows/spring-gradle-pull-request-build.yml@v5
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 40dfecca8b..be8a9e40c6 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -12,7 +12,7 @@ jobs:
       contents: write
       issues: write
 
-    uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-release.yml@v4
+    uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-release.yml@v5
     secrets:
       GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
       DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}

From 363be061d9f2ea16c7fe1904067c6c18d94979ff Mon Sep 17 00:00:00 2001
From: Artem Bilan 
Date: Mon, 18 Nov 2024 16:45:57 -0500
Subject: [PATCH 622/737] Remove `3.1.x` branch from Dependabot updates

The version `3.1.8` is the last OSS version - no updates from now on

* Also remove dependencies which we don't manage anymore
---
 .github/dependabot.yml | 49 ------------------------------------------
 1 file changed, 49 deletions(-)

diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 5a8f11e0aa..3db6fde556 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -28,57 +28,8 @@ updates:
          - org.hibernate.validator:hibernate-validator
          - org.apache.httpcomponents.client5:httpclient5
          - org.awaitility:awaitility
-         - org.xerial.snappy:snappy-java
-         - org.lz4:lz4-java
-         - com.github.luben:zstd-jni
-
-  - package-ecosystem: gradle
-    target-branch: 3.1.x
-    directory: /
-    schedule:
-      interval: weekly
-      day: saturday
-    ignore:
-      - dependency-name: '*'
-        update-types:
-          - version-update:semver-major
-          - version-update:semver-minor
-    open-pull-requests-limit: 10
-    labels:
-      - 'type: dependency-upgrade'
-    groups:
-      development-dependencies:
-        update-types:
-          - patch
-        patterns:
-          - com.gradle.*
-          - com.github.spotbugs
-          - io.spring.*
-          - org.ajoberstar.grgit
-          - org.antora
-          - io.micrometer:micrometer-docs-generator
-          - com.willowtreeapps.assertk:assertk-jvm
-          - org.hibernate.validator:hibernate-validator
-          - org.apache.httpcomponents.client5:httpclient5
-          - org.awaitility:awaitility
-          - org.xerial.snappy:snappy-java
-          - org.lz4:lz4-java
-          - com.github.luben:zstd-jni
-
-  - package-ecosystem: github-actions
-    directory: /
-    schedule:
-      interval: weekly
-      day: saturday
-    labels:
-      - 'type: task'
-    groups:
-      development-dependencies:
-        patterns:
-          - '*'
 
   - package-ecosystem: github-actions
-    target-branch: 3.1.x
     directory: /
     schedule:
       interval: weekly

From 1661ee91560f4b6b9ce0a60ea424a3eca4c580a1 Mon Sep 17 00:00:00 2001
From: Spring Builds 
Date: Mon, 18 Nov 2024 22:07:05 +0000
Subject: [PATCH 623/737] [artifactory-release] Release version 3.2.0

---
 gradle.properties | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gradle.properties b/gradle.properties
index 6bc0422ab1..18e7cac5e2 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
-version=3.2.0-SNAPSHOT
+version=3.2.0
 org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8
 org.gradle.daemon=true
 org.gradle.caching=true

From 62fd4beaf5015afaa69d6856a1657cf52db40b1b Mon Sep 17 00:00:00 2001
From: Spring Builds 
Date: Mon, 18 Nov 2024 22:07:05 +0000
Subject: [PATCH 624/737] [artifactory-release] Next development version

---
 gradle.properties | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gradle.properties b/gradle.properties
index 18e7cac5e2..2b07979582 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
-version=3.2.0
+version=3.2.1-SNAPSHOT
 org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8
 org.gradle.daemon=true
 org.gradle.caching=true

From 0d31fa5f8395971aa70d660ab510bce0688a7ea7 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 22 Nov 2024 22:37:54 -0500
Subject: [PATCH 625/737] Bump org.testcontainers:testcontainers-bom from
 1.20.3 to 1.20.4 (#2912)

Bumps [org.testcontainers:testcontainers-bom](https://github.com/testcontainers/testcontainers-java) from 1.20.3 to 1.20.4.
- [Release notes](https://github.com/testcontainers/testcontainers-java/releases)
- [Changelog](https://github.com/testcontainers/testcontainers-java/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testcontainers/testcontainers-java/compare/1.20.3...1.20.4)

---
updated-dependencies:
- dependency-name: org.testcontainers:testcontainers-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index f167ef1ac5..d6fd1191ef 100644
--- a/build.gradle
+++ b/build.gradle
@@ -69,7 +69,7 @@ ext {
 	springDataVersion = '2024.1.0'
 	springRetryVersion = '2.0.10'
 	springVersion = '6.2.0'
-	testcontainersVersion = '1.20.3'
+	testcontainersVersion = '1.20.4'
 
 	javaProjects = subprojects - project(':spring-amqp-bom')
 }

From c182d2adf205d43ebbf3ef700b0148d2b8ae7aca Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 22 Nov 2024 22:38:21 -0500
Subject: [PATCH 626/737] Bump org.apache.logging.log4j:log4j-bom from 2.24.1
 to 2.24.2 (#2913)

Bumps [org.apache.logging.log4j:log4j-bom](https://github.com/apache/logging-log4j2) from 2.24.1 to 2.24.2.
- [Release notes](https://github.com/apache/logging-log4j2/releases)
- [Changelog](https://github.com/apache/logging-log4j2/blob/2.x/RELEASE-NOTES.adoc)
- [Commits](https://github.com/apache/logging-log4j2/compare/rel/2.24.1...rel/2.24.2)

---
updated-dependencies:
- dependency-name: org.apache.logging.log4j:log4j-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index d6fd1191ef..5bdd577d03 100644
--- a/build.gradle
+++ b/build.gradle
@@ -57,7 +57,7 @@ ext {
 	junit4Version = '4.13.2'
 	junitJupiterVersion = '5.11.3'
 	kotlinCoroutinesVersion = '1.8.1'
-	log4jVersion = '2.24.1'
+	log4jVersion = '2.24.2'
 	logbackVersion = '1.5.12'
 	micrometerDocsVersion = '1.0.4'
 	micrometerVersion = '1.14.1'

From 0128f7e3bd59975d901a8181eb81ffa058c672b5 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 2 Dec 2024 13:33:32 -0500
Subject: [PATCH 627/737] Bump com.fasterxml.jackson:jackson-bom from 2.18.1 to
 2.18.2

Bumps [com.fasterxml.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) from 2.18.1 to 2.18.2.
- [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-2.18.1...jackson-bom-2.18.2)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson:jackson-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index 5bdd577d03..a2056286a1 100644
--- a/build.gradle
+++ b/build.gradle
@@ -52,7 +52,7 @@ ext {
 	commonsPoolVersion = '2.12.0'
 	hamcrestVersion = '3.0'
 	hibernateValidationVersion = '8.0.1.Final'
-	jacksonBomVersion = '2.18.1'
+	jacksonBomVersion = '2.18.2'
 	jaywayJsonPathVersion = '2.9.0'
 	junit4Version = '4.13.2'
 	junitJupiterVersion = '5.11.3'

From ced72edb6c0ed0b41a132207682699115a7c56c2 Mon Sep 17 00:00:00 2001
From: Artem Bilan 
Date: Tue, 3 Dec 2024 12:13:46 -0500
Subject: [PATCH 628/737] GH-2917: Add
 `AbstractConnectionFactory.setAddresses(List)`

Fixes: https://github.com/spring-projects/spring-amqp/issues/2917

In Spring Boot `3.4.0` the `org.springframework.boot.autoconfigure.amqp.RabbitProperties#getAddresses()`
returns a `List` while it previously returned a `String`.
This makes it incompatible with `AbstractConnectionFactory.setAddresses` which accepts a `String`.
Previously we could do `cachingConnectionFactory.setAddresses(rabbitProperties.getAddresses());`,
but now have to go with a `cachingConnectionFactory.setAddresses(String.join(",", rabbitProperties.getAddresses()))`

* Expose `AbstractConnectionFactory.setAddresses(List)` for the mentioned convenience to be able to use `List`
as an alternative to comma-separated string
---
 .../rabbit/connection/AbstractConnectionFactory.java  | 11 ++++++++++-
 .../connection/AbstractConnectionFactoryTests.java    |  3 ++-
 2 files changed, 12 insertions(+), 2 deletions(-)

diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java
index caa57d6c92..b7ecb1a80d 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java
@@ -344,7 +344,16 @@ public int getPort() {
 
 	/**
 	 * Set addresses for clustering. This property overrides the host+port properties if not empty.
-	 * @param addresses list of addresses with form "host[:port],..."
+	 * @param addresses list of addresses in form {@code host[:port]}.
+	 * @since 3.2.1
+	 */
+	public void setAddresses(List addresses) {
+		Assert.notEmpty(addresses, "Addresses must not be empty");
+		setAddresses(String.join(",", addresses));
+	}
+	/**
+	 * Set addresses for clustering. This property overrides the host+port properties if not empty.
+	 * @param addresses list of addresses with form {@code host1[:port1],host2[:port2],...}.
 	 */
 	public void setAddresses(String addresses) {
 		this.lock.lock();
diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactoryTests.java
index e41ea8635d..097ddf9632 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactoryTests.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactoryTests.java
@@ -33,6 +33,7 @@
 import static org.mockito.Mockito.verify;
 
 import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -110,7 +111,7 @@ public void onClose(Connection connection) {
 
 		verify(mockConnectionFactory, times(1)).newConnection(any(ExecutorService.class), anyString());
 
-		connectionFactory.setAddresses("foo:5672,bar:5672");
+		connectionFactory.setAddresses(List.of("foo:5672", "bar:5672"));
 		connectionFactory.setAddressShuffleMode(AddressShuffleMode.NONE);
 		con = connectionFactory.createConnection();
 		assertThat(called.get()).isEqualTo(1);

From 162ef4a60e31daeaf64f043063fed965ee4bc69b Mon Sep 17 00:00:00 2001
From: Artem Bilan 
Date: Fri, 6 Dec 2024 09:43:12 -0500
Subject: [PATCH 629/737] Attempt to fix race condition in the
 `CachingConnectionFactory`

There is a logic in the `CachingConnectionFactory.destroy()` where we wait for `inFlightAsyncCloses`.
However, it looks like we first close the main connection with all its channels.
Therefore, we may end up with a `ConcurrentModificationException` when we iterate channels list for clean up,
but in-flight requests still wait for their confirms, therefore they may come back to the cache meanwhile we are trying to clean up
---
 .../rabbit/connection/CachingConnectionFactory.java    |  4 ++--
 ...itTemplatePublisherCallbacksIntegration1Tests.java} |  4 ++--
 ...itTemplatePublisherCallbacksIntegration2Tests.java} |  8 ++++----
 ...itTemplatePublisherCallbacksIntegration3Tests.java} | 10 +++++-----
 4 files changed, 13 insertions(+), 13 deletions(-)
 rename spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/{RabbitTemplatePublisherCallbacksIntegrationTests.java => RabbitTemplatePublisherCallbacksIntegration1Tests.java} (99%)
 rename spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/{RabbitTemplatePublisherCallbacksIntegrationTests2.java => RabbitTemplatePublisherCallbacksIntegration2Tests.java} (96%)
 rename spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/{RabbitTemplatePublisherCallbacksIntegrationTests3.java => RabbitTemplatePublisherCallbacksIntegration3Tests.java} (95%)

diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java
index 0aade48321..302b18a5cd 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java
@@ -861,8 +861,6 @@ private void refreshProxyConnection(ChannelCachingConnectionProxy connection) {
 	 */
 	@Override
 	public final void destroy() {
-		super.destroy();
-		resetConnection();
 		if (getContextStopped()) {
 			this.stopped = true;
 			this.connectionLock.lock();
@@ -890,6 +888,8 @@ public final void destroy() {
 				this.connectionLock.unlock();
 			}
 		}
+		super.destroy();
+		resetConnection();
 	}
 
 	/**
diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration1Tests.java
similarity index 99%
rename from spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests.java
rename to spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration1Tests.java
index 1429b7f5e6..fef647063b 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration1Tests.java
@@ -95,8 +95,8 @@
  * @since 1.1
  *
  */
-@RabbitAvailable(queues = RabbitTemplatePublisherCallbacksIntegrationTests.ROUTE)
-public class RabbitTemplatePublisherCallbacksIntegrationTests {
+@RabbitAvailable(queues = RabbitTemplatePublisherCallbacksIntegration1Tests.ROUTE)
+public class RabbitTemplatePublisherCallbacksIntegration1Tests {
 
 	public static final String ROUTE = "test.queue.RabbitTemplatePublisherCallbacksIntegrationTests";
 
diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests2.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration2Tests.java
similarity index 96%
rename from spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests2.java
rename to spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration2Tests.java
index 450c1566a8..06837b9895 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests2.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration2Tests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016-2022 the original author or authors.
+ * Copyright 2016-2024 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.
@@ -41,9 +41,9 @@
  * @since 1.6
  *
  */
-@RabbitAvailable(queues = { RabbitTemplatePublisherCallbacksIntegrationTests2.ROUTE,
-		RabbitTemplatePublisherCallbacksIntegrationTests2.ROUTE2 })
-public class RabbitTemplatePublisherCallbacksIntegrationTests2 {
+@RabbitAvailable(queues = { RabbitTemplatePublisherCallbacksIntegration2Tests.ROUTE,
+		RabbitTemplatePublisherCallbacksIntegration2Tests.ROUTE2 })
+public class RabbitTemplatePublisherCallbacksIntegration2Tests {
 
 	public static final String ROUTE = "test.queue.RabbitTemplatePublisherCallbacksIntegrationTests2";
 
diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests3.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration3Tests.java
similarity index 95%
rename from spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests3.java
rename to spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration3Tests.java
index 8e7fcef70e..d9f1dddd47 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegrationTests3.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration3Tests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-2021 the original author or authors.
+ * Copyright 2018-2024 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.
@@ -41,10 +41,10 @@
  * @since 2.1
  *
  */
-@RabbitAvailable(queues = { RabbitTemplatePublisherCallbacksIntegrationTests3.QUEUE1,
-		RabbitTemplatePublisherCallbacksIntegrationTests3.QUEUE2,
-		RabbitTemplatePublisherCallbacksIntegrationTests3.QUEUE3 })
-public class RabbitTemplatePublisherCallbacksIntegrationTests3 {
+@RabbitAvailable(queues = { RabbitTemplatePublisherCallbacksIntegration3Tests.QUEUE1,
+		RabbitTemplatePublisherCallbacksIntegration3Tests.QUEUE2,
+		RabbitTemplatePublisherCallbacksIntegration3Tests.QUEUE3 })
+public class RabbitTemplatePublisherCallbacksIntegration3Tests {
 
 	public static final String QUEUE1 = "synthetic.nack";
 

From db59442ea26baad39044cc70baec5856a2f1bcc8 Mon Sep 17 00:00:00 2001
From: Artem Bilan 
Date: Thu, 12 Dec 2024 11:22:46 -0500
Subject: [PATCH 630/737] Add `@DirtiesContext` to tests to clean up app ctx
 cache

And attempt to mitigate out of memory error on CI/CD
---
 .../support/converter/Jackson2JsonMessageConverterTests.java  | 4 +++-
 .../support/converter/Jackson2XmlMessageConverterTests.java   | 4 +++-
 .../rabbit/stream/config/SuperStreamProvisioningTests.java    | 4 +++-
 .../rabbit/stream/listener/SuperStreamConcurrentSACTests.java | 4 +++-
 .../rabbit/stream/listener/SuperStreamSACTests.java           | 4 +++-
 .../amqp/rabbit/test/context/SpringRabbitTestTests.java       | 4 +++-
 .../amqp/rabbit/test/examples/TestRabbitTemplateTests.java    | 2 ++
 ...ContentTypeDelegatingMessageConverterIntegrationTests.java | 4 +++-
 .../amqp/rabbit/annotation/OptionalPayloadTests.java          | 4 +++-
 .../amqp/rabbit/listener/AsyncReplyToTests.java               | 4 +++-
 .../amqp/rabbit/listener/BrokerEventListenerTests.java        | 3 ++-
 .../springframework/amqp/rabbit/listener/DlqExpiryTests.java  | 4 +++-
 .../adapter/BatchMessagingMessageListenerAdapterTests.java    | 4 +++-
 .../amqp/rabbit/support/micrometer/ObservationTests.java      | 2 ++
 14 files changed, 39 insertions(+), 12 deletions(-)

diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverterTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverterTests.java
index 37e274d314..574fecc06f 100644
--- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverterTests.java
+++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverterTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2024 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.
@@ -34,6 +34,7 @@
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.core.ParameterizedTypeReference;
 import org.springframework.data.web.JsonPath;
+import org.springframework.test.annotation.DirtiesContext;
 import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
 import org.springframework.util.MimeTypeUtils;
 
@@ -54,6 +55,7 @@
  * @author Artem Bilan
  */
 @SpringJUnitConfig
+@DirtiesContext
 public class Jackson2JsonMessageConverterTests {
 
 	public static final String TRUSTED_PACKAGE = Jackson2JsonMessageConverterTests.class.getPackage().getName();
diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2XmlMessageConverterTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2XmlMessageConverterTests.java
index d5e3825a0c..836ee45ca0 100644
--- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2XmlMessageConverterTests.java
+++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2XmlMessageConverterTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-2019 the original author or authors.
+ * Copyright 2018-2024 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.
@@ -31,6 +31,7 @@
 import org.springframework.amqp.core.MessageProperties;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.test.annotation.DirtiesContext;
 import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
 
 import com.fasterxml.jackson.databind.ser.BeanSerializerFactory;
@@ -43,6 +44,7 @@
  * @since 2.1
  */
 @SpringJUnitConfig
+@DirtiesContext
 public class Jackson2XmlMessageConverterTests {
 
 	public static final String TRUSTED_PACKAGE = Jackson2XmlMessageConverterTests.class.getPackage().getName();
diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamProvisioningTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamProvisioningTests.java
index b87daf8db8..75880c5e2d 100644
--- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamProvisioningTests.java
+++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamProvisioningTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022 the original author or authors.
+ * Copyright 2022-2024 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.
@@ -32,6 +32,7 @@
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.test.annotation.DirtiesContext;
 import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
 
 /**
@@ -40,6 +41,7 @@
  *
  */
 @SpringJUnitConfig
+@DirtiesContext
 public class SuperStreamProvisioningTests extends AbstractTestContainerTests {
 
 	@Test
diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamConcurrentSACTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamConcurrentSACTests.java
index c7d34dd83e..50e1ff1058 100644
--- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamConcurrentSACTests.java
+++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamConcurrentSACTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022-2023 the original author or authors.
+ * Copyright 2022-2024 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.
@@ -37,6 +37,7 @@
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.rabbit.stream.config.SuperStream;
+import org.springframework.test.annotation.DirtiesContext;
 import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
 
 import com.rabbitmq.stream.Environment;
@@ -49,6 +50,7 @@
  *
  */
 @SpringJUnitConfig
+@DirtiesContext
 public class SuperStreamConcurrentSACTests extends AbstractTestContainerTests {
 
 	@Test
diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java
index 1daaca2d33..a474f6926e 100644
--- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java
+++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022-2023 the original author or authors.
+ * Copyright 2022-2024 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.
@@ -45,6 +45,7 @@
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Scope;
 import org.springframework.rabbit.stream.config.SuperStream;
+import org.springframework.test.annotation.DirtiesContext;
 import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
 
 import com.rabbitmq.stream.Environment;
@@ -57,6 +58,7 @@
  *
  */
 @SpringJUnitConfig
+@DirtiesContext
 public class SuperStreamSACTests extends AbstractTestContainerTests {
 
 	@Test
diff --git a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/context/SpringRabbitTestTests.java b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/context/SpringRabbitTestTests.java
index b4c2fc7e01..697e984a0f 100644
--- a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/context/SpringRabbitTestTests.java
+++ b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/context/SpringRabbitTestTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 the original author or authors.
+ * Copyright 2020-2024 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.
@@ -29,6 +29,7 @@
 import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.test.annotation.DirtiesContext;
 import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
 
 /**
@@ -39,6 +40,7 @@
 @RabbitAvailable
 @SpringJUnitConfig
 @SpringRabbitTest
+@DirtiesContext
 public class SpringRabbitTestTests {
 
 	@Autowired
diff --git a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/TestRabbitTemplateTests.java b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/TestRabbitTemplateTests.java
index 4d8c69586b..6ee389b821 100644
--- a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/TestRabbitTemplateTests.java
+++ b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/TestRabbitTemplateTests.java
@@ -39,6 +39,7 @@
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.test.annotation.DirtiesContext;
 import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
 
 import com.rabbitmq.client.AMQP;
@@ -53,6 +54,7 @@
  *
  */
 @SpringJUnitConfig
+@DirtiesContext
 public class TestRabbitTemplateTests {
 
 	@Autowired
diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ContentTypeDelegatingMessageConverterIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ContentTypeDelegatingMessageConverterIntegrationTests.java
index 91a23421ba..3ac031bcd0 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ContentTypeDelegatingMessageConverterIntegrationTests.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ContentTypeDelegatingMessageConverterIntegrationTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 the original author or authors.
+ * Copyright 2020-2024 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.
@@ -44,6 +44,7 @@
 import org.springframework.messaging.MessageHeaders;
 import org.springframework.messaging.handler.annotation.SendTo;
 import org.springframework.messaging.support.MessageBuilder;
+import org.springframework.test.annotation.DirtiesContext;
 import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
 import org.springframework.util.MimeType;
 
@@ -54,6 +55,7 @@
  */
 @RabbitAvailable
 @SpringJUnitConfig
+@DirtiesContext
 public class ContentTypeDelegatingMessageConverterIntegrationTests {
 
 	@Autowired
diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/OptionalPayloadTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/OptionalPayloadTests.java
index 1d5a476433..56790f4eab 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/OptionalPayloadTests.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/OptionalPayloadTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022 the original author or authors.
+ * Copyright 2022-2024 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.
@@ -40,6 +40,7 @@
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.messaging.handler.annotation.Payload;
+import org.springframework.test.annotation.DirtiesContext;
 import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
@@ -52,6 +53,7 @@
  */
 @SpringJUnitConfig
 @RabbitAvailable(queues = { "op.1", "op.2" })
+@DirtiesContext
 public class OptionalPayloadTests {
 
 	@Test
diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/AsyncReplyToTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/AsyncReplyToTests.java
index e5c43a5fd7..23e6bb17d5 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/AsyncReplyToTests.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/AsyncReplyToTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021-2022 the original author or authors.
+ * Copyright 2021-2024 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.
@@ -44,6 +44,7 @@
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.test.annotation.DirtiesContext;
 import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
 
 import com.rabbitmq.client.Channel;
@@ -55,6 +56,7 @@
  */
 @SpringJUnitConfig
 @RabbitAvailable(queues = { "async1", "async2" })
+@DirtiesContext
 public class AsyncReplyToTests {
 
 	@Test
diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BrokerEventListenerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BrokerEventListenerTests.java
index 2541493c7a..d5473bc3cb 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BrokerEventListenerTests.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BrokerEventListenerTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-2019 the original author or authors.
+ * Copyright 2018-2024 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.
@@ -48,6 +48,7 @@
  */
 @SpringJUnitConfig
 @RabbitAvailable
+@DirtiesContext
 public class BrokerEventListenerTests {
 
 	@Autowired
diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DlqExpiryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DlqExpiryTests.java
index 729a66e825..4b5d8ca51b 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DlqExpiryTests.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DlqExpiryTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-2019 the original author or authors.
+ * Copyright 2018-2024 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.
@@ -38,6 +38,7 @@
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.test.annotation.DirtiesContext;
 import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
 
 /**
@@ -47,6 +48,7 @@
  */
 @RabbitAvailable
 @SpringJUnitConfig
+@DirtiesContext
 public class DlqExpiryTests {
 
 	@Autowired
diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapterTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapterTests.java
index d12f1d5a51..978551e288 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapterTests.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapterTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022-2023 the original author or authors.
+ * Copyright 2022-2024 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.
@@ -44,6 +44,7 @@
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.test.annotation.DirtiesContext;
 import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
@@ -57,6 +58,7 @@
  */
 @SpringJUnitConfig
 @RabbitAvailable(queues = "test.batchQueue")
+@DirtiesContext
 public class BatchMessagingMessageListenerAdapterTests {
 
 	@Test
diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java
index fe1b38995a..0b6cc28325 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java
@@ -44,6 +44,7 @@
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.lang.Nullable;
+import org.springframework.test.annotation.DirtiesContext;
 import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
 
 import io.micrometer.common.KeyValues;
@@ -74,6 +75,7 @@
  */
 @SpringJUnitConfig
 @RabbitAvailable(queues = { "observation.testQ1", "observation.testQ2" })
+@DirtiesContext
 public class ObservationTests {
 
 	@Test

From 932492787d2a3974638aef0fc017a084545a382e Mon Sep 17 00:00:00 2001
From: Artem Bilan 
Date: Thu, 12 Dec 2024 12:11:29 -0500
Subject: [PATCH 631/737] GH-2914: Make `delivery_tag` as a high cardinality
 observation tag

Fixes: https://github.com/spring-projects/spring-amqp/issues/2914

The `delivery_tag` on consumer side is really per message,
so that makes too many metric timers when this property is exposed as a low cardinality tag.

* Fix `RabbitListenerObservation` moving the `ListenerLowCardinalityTags.DELIVERY_TAG` into the `ListenerHighCardinalityTags.DELIVERY_TAG`
* Leave `ListenerLowCardinalityTags.DELIVERY_TAG` as deprecated to avoid compatibility issues,
but exclude it from the `getLowCardinalityKeyNames()` implementation
---
 .../micrometer/RabbitListenerObservation.java | 51 ++++++++++++++++---
 .../ObservationIntegrationTests.java          |  7 ++-
 2 files changed, 48 insertions(+), 10 deletions(-)

diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java
index a9bb6dabd8..47efa0be3a 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java
@@ -16,6 +16,8 @@
 
 package org.springframework.amqp.rabbit.support.micrometer;
 
+import java.util.Arrays;
+
 import io.micrometer.common.KeyValues;
 import io.micrometer.common.docs.KeyName;
 import io.micrometer.observation.Observation.Context;
@@ -45,7 +47,14 @@ public Class> getDefaultConve
 
 		@Override
 		public KeyName[] getLowCardinalityKeyNames() {
-			return ListenerLowCardinalityTags.values();
+			return Arrays.stream(ListenerLowCardinalityTags.values())
+					.filter((key) -> !ListenerLowCardinalityTags.DELIVERY_TAG.equals(key))
+					.toArray(KeyName[]::new);
+		}
+
+		@Override
+		public KeyName[] getHighCardinalityKeyNames() {
+			return ListenerHighCardinalityTags.values();
 		}
 
 	};
@@ -83,8 +92,33 @@ public String asString() {
 
 		/**
 		 * The delivery tag.
+		 * After deprecation this key is not exposed as a low cardinality tag.
 		 *
 		 * @since 3.2
+		 *
+		 * @deprecated in favor of {@link ListenerHighCardinalityTags#DELIVERY_TAG}
+		 */
+		@Deprecated(since = "3.2.1", forRemoval = true)
+		DELIVERY_TAG {
+
+			@Override
+			public String asString() {
+				return "messaging.rabbitmq.message.delivery_tag";
+			}
+
+		}
+
+	}
+
+	/**
+	 * High cardinality tags.
+	 *
+	 * @since 3.2.1
+	 */
+	public enum ListenerHighCardinalityTags implements KeyName {
+
+		/**
+		 * The delivery tag.
 		 */
 		DELIVERY_TAG {
 
@@ -97,6 +131,7 @@ public String asString() {
 
 	}
 
+
 	/**
 	 * Default {@link RabbitListenerObservationConvention} for Rabbit listener key values.
 	 */
@@ -112,12 +147,16 @@ public static class DefaultRabbitListenerObservationConvention implements Rabbit
 		public KeyValues getLowCardinalityKeyValues(RabbitMessageReceiverContext context) {
 			final var messageProperties = context.getCarrier().getMessageProperties();
 			return KeyValues.of(
-					RabbitListenerObservation.ListenerLowCardinalityTags.LISTENER_ID.asString(), context.getListenerId(),
+					RabbitListenerObservation.ListenerLowCardinalityTags.LISTENER_ID.asString(),
+					context.getListenerId(),
 					RabbitListenerObservation.ListenerLowCardinalityTags.DESTINATION_NAME.asString(),
-					messageProperties.getConsumerQueue(),
-					RabbitListenerObservation.ListenerLowCardinalityTags.DELIVERY_TAG.asString(),
-					String.valueOf(messageProperties.getDeliveryTag())
-			);
+					messageProperties.getConsumerQueue());
+		}
+
+		@Override
+		public KeyValues getHighCardinalityKeyValues(RabbitMessageReceiverContext context) {
+			return KeyValues.of(RabbitListenerObservation.ListenerHighCardinalityTags.DELIVERY_TAG.asString(),
+					String.valueOf(context.getCarrier().getMessageProperties().getDeliveryTag()));
 		}
 
 		@Override
diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java
index 920deec403..48e981c2f1 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java
@@ -47,6 +47,7 @@
 /**
  * @author Artem Bilan
  * @author Gary Russell
+ *
  * @since 3.0
  */
 @RabbitAvailable(queues = { "int.observation.testQ1", "int.observation.testQ2" })
@@ -120,15 +121,13 @@ public SampleTestRunnerConsumer yourCode() {
 					.hasTimerWithNameAndTags("spring.rabbit.listener",
 							KeyValues.of(
 									KeyValue.of("spring.rabbit.listener.id", "obs1"),
-									KeyValue.of("messaging.destination.name", "int.observation.testQ1"),
-									KeyValue.of("messaging.rabbitmq.message.delivery_tag", "1")
+									KeyValue.of("messaging.destination.name", "int.observation.testQ1")
 							)
 					)
 					.hasTimerWithNameAndTags("spring.rabbit.listener",
 							KeyValues.of(
 									KeyValue.of("spring.rabbit.listener.id", "obs2"),
-									KeyValue.of("messaging.destination.name", "int.observation.testQ2"),
-									KeyValue.of("messaging.rabbitmq.message.delivery_tag", "1")
+									KeyValue.of("messaging.destination.name", "int.observation.testQ2")
 							)
 					);
 		};

From b6e7235e6e4e100ded89157b7217844143add7c6 Mon Sep 17 00:00:00 2001
From: Artem Bilan 
Date: Thu, 12 Dec 2024 12:18:27 -0500
Subject: [PATCH 632/737] Fix `RabbitListenerObservation` for docs generation
 compatibility

---
 .../support/micrometer/RabbitListenerObservation.java       | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java
index 47efa0be3a..19e75baf0b 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java
@@ -16,8 +16,6 @@
 
 package org.springframework.amqp.rabbit.support.micrometer;
 
-import java.util.Arrays;
-
 import io.micrometer.common.KeyValues;
 import io.micrometer.common.docs.KeyName;
 import io.micrometer.observation.Observation.Context;
@@ -47,9 +45,7 @@ public Class> getDefaultConve
 
 		@Override
 		public KeyName[] getLowCardinalityKeyNames() {
-			return Arrays.stream(ListenerLowCardinalityTags.values())
-					.filter((key) -> !ListenerLowCardinalityTags.DELIVERY_TAG.equals(key))
-					.toArray(KeyName[]::new);
+			return ListenerLowCardinalityTags.values();
 		}
 
 		@Override

From 6da994aba93b38826654dcf86440eb490eba59bc Mon Sep 17 00:00:00 2001
From: Artem Bilan 
Date: Thu, 12 Dec 2024 12:22:02 -0500
Subject: [PATCH 633/737] GH-2895: Mention `endpoint.setId()` in the
 `registration.adoc`

Fixes: https://github.com/spring-projects/spring-amqp/issues/2895

Since `id` is required for the `SimpleRabbitListenerEndpoint` definition,
it is better to show it in the docs sample and mention its importance.
---
 .../async-annotation-driven/registration.adoc                  | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/registration.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/registration.adoc
index d1458af838..46db7e95b5 100644
--- a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/registration.adoc
+++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/registration.adoc
@@ -14,6 +14,7 @@ public class AppConfig implements RabbitListenerConfigurer {
     @Override
     public void configureRabbitListeners(RabbitListenerEndpointRegistrar registrar) {
         SimpleRabbitListenerEndpoint endpoint = new SimpleRabbitListenerEndpoint();
+		endpoint.setId("someRabbitListenerEndpoint");
         endpoint.setQueueNames("anotherQueue");
         endpoint.setMessageListener(message -> {
             // processing
@@ -25,5 +26,7 @@ public class AppConfig implements RabbitListenerConfigurer {
 
 In the preceding example, we used `SimpleRabbitListenerEndpoint`, which provides the actual `MessageListener` to invoke, but you could just as well build your own endpoint variant to describe a custom invocation mechanism.
 
+NOTE: the `id` property is required for `SimpleRabbitListenerEndpoint` definition.
+
 It should be noted that you could just as well skip the use of `@RabbitListener` altogether and register your endpoints programmatically through `RabbitListenerConfigurer`.
 

From 5d7c382d8ef83f547f685244338e884f5de2badb Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 14 Dec 2024 02:19:00 +0000
Subject: [PATCH 634/737] Bump io.micrometer:micrometer-bom from 1.14.1 to
 1.14.2 (#2927)

Bumps [io.micrometer:micrometer-bom](https://github.com/micrometer-metrics/micrometer) from 1.14.1 to 1.14.2.
- [Release notes](https://github.com/micrometer-metrics/micrometer/releases)
- [Commits](https://github.com/micrometer-metrics/micrometer/compare/v1.14.1...v1.14.2)

---
updated-dependencies:
- dependency-name: io.micrometer:micrometer-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index a2056286a1..0d3689a3e7 100644
--- a/build.gradle
+++ b/build.gradle
@@ -60,7 +60,7 @@ ext {
 	log4jVersion = '2.24.2'
 	logbackVersion = '1.5.12'
 	micrometerDocsVersion = '1.0.4'
-	micrometerVersion = '1.14.1'
+	micrometerVersion = '1.14.2'
 	micrometerTracingVersion = '1.4.0'
 	mockitoVersion = '5.14.2'
 	rabbitmqStreamVersion = '0.18.0'

From 40bded4550e40cbe332294a9676eda5cc598bb96 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 14 Dec 2024 02:19:12 +0000
Subject: [PATCH 635/737] Bump org.hibernate.validator:hibernate-validator
 (#2922)

Bumps the development-dependencies group with 1 update: [org.hibernate.validator:hibernate-validator](https://github.com/hibernate/hibernate-validator).


Updates `org.hibernate.validator:hibernate-validator` from 8.0.1.Final to 8.0.2.Final
- [Changelog](https://github.com/hibernate/hibernate-validator/blob/8.0.2.Final/changelog.txt)
- [Commits](https://github.com/hibernate/hibernate-validator/compare/8.0.1.Final...8.0.2.Final)

---
updated-dependencies:
- dependency-name: org.hibernate.validator:hibernate-validator
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index 0d3689a3e7..b10f980174 100644
--- a/build.gradle
+++ b/build.gradle
@@ -51,7 +51,7 @@ ext {
 	commonsHttpClientVersion = '5.4.1'
 	commonsPoolVersion = '2.12.0'
 	hamcrestVersion = '3.0'
-	hibernateValidationVersion = '8.0.1.Final'
+	hibernateValidationVersion = '8.0.2.Final'
 	jacksonBomVersion = '2.18.2'
 	jaywayJsonPathVersion = '2.9.0'
 	junit4Version = '4.13.2'

From 12a892ee77981f325104192fbd97fa7770677432 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 14 Dec 2024 02:19:48 +0000
Subject: [PATCH 636/737] Bump org.springframework.retry:spring-retry from
 2.0.10 to 2.0.11 (#2923)

Bumps [org.springframework.retry:spring-retry](https://github.com/spring-projects/spring-retry) from 2.0.10 to 2.0.11.
- [Release notes](https://github.com/spring-projects/spring-retry/releases)
- [Commits](https://github.com/spring-projects/spring-retry/compare/v2.0.10...v2.0.11)

---
updated-dependencies:
- dependency-name: org.springframework.retry:spring-retry
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index b10f980174..4c4f9094be 100644
--- a/build.gradle
+++ b/build.gradle
@@ -67,7 +67,7 @@ ext {
 	rabbitmqVersion = '5.22.0'
 	reactorVersion = '2024.0.0'
 	springDataVersion = '2024.1.0'
-	springRetryVersion = '2.0.10'
+	springRetryVersion = '2.0.11'
 	springVersion = '6.2.0'
 	testcontainersVersion = '1.20.4'
 

From 2bbd4929c130f6870a020fa819b3f3e524d3baea Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 14 Dec 2024 02:19:58 +0000
Subject: [PATCH 637/737] Bump org.apache.logging.log4j:log4j-bom from 2.24.2
 to 2.24.3 (#2925)

Bumps [org.apache.logging.log4j:log4j-bom](https://github.com/apache/logging-log4j2) from 2.24.2 to 2.24.3.
- [Release notes](https://github.com/apache/logging-log4j2/releases)
- [Changelog](https://github.com/apache/logging-log4j2/blob/2.x/RELEASE-NOTES.adoc)
- [Commits](https://github.com/apache/logging-log4j2/compare/rel/2.24.2...rel/2.24.3)

---
updated-dependencies:
- dependency-name: org.apache.logging.log4j:log4j-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index 4c4f9094be..8eba02a4c4 100644
--- a/build.gradle
+++ b/build.gradle
@@ -57,7 +57,7 @@ ext {
 	junit4Version = '4.13.2'
 	junitJupiterVersion = '5.11.3'
 	kotlinCoroutinesVersion = '1.8.1'
-	log4jVersion = '2.24.2'
+	log4jVersion = '2.24.3'
 	logbackVersion = '1.5.12'
 	micrometerDocsVersion = '1.0.4'
 	micrometerVersion = '1.14.2'

From c63752419a0b5f5f87b1d782d2a558f509bb1005 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 14 Dec 2024 02:20:06 +0000
Subject: [PATCH 638/737] Bump io.projectreactor:reactor-bom from 2024.0.0 to
 2024.0.1 (#2924)

Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2024.0.0 to 2024.0.1.
- [Release notes](https://github.com/reactor/reactor/releases)
- [Commits](https://github.com/reactor/reactor/compare/2024.0.0...2024.0.1)

---
updated-dependencies:
- dependency-name: io.projectreactor:reactor-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index 8eba02a4c4..ed6642aeeb 100644
--- a/build.gradle
+++ b/build.gradle
@@ -65,7 +65,7 @@ ext {
 	mockitoVersion = '5.14.2'
 	rabbitmqStreamVersion = '0.18.0'
 	rabbitmqVersion = '5.22.0'
-	reactorVersion = '2024.0.0'
+	reactorVersion = '2024.0.1'
 	springDataVersion = '2024.1.0'
 	springRetryVersion = '2.0.11'
 	springVersion = '6.2.0'

From 0bca0206aa0f62d76a3a4e9bbad5c415540f333f Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 14 Dec 2024 02:23:29 +0000
Subject: [PATCH 639/737] Bump io.micrometer:micrometer-tracing-bom from 1.4.0
 to 1.4.1 (#2929)

Bumps [io.micrometer:micrometer-tracing-bom](https://github.com/micrometer-metrics/tracing) from 1.4.0 to 1.4.1.
- [Release notes](https://github.com/micrometer-metrics/tracing/releases)
- [Commits](https://github.com/micrometer-metrics/tracing/compare/v1.4.0...v1.4.1)

---
updated-dependencies:
- dependency-name: io.micrometer:micrometer-tracing-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index ed6642aeeb..587d5e3af2 100644
--- a/build.gradle
+++ b/build.gradle
@@ -61,7 +61,7 @@ ext {
 	logbackVersion = '1.5.12'
 	micrometerDocsVersion = '1.0.4'
 	micrometerVersion = '1.14.2'
-	micrometerTracingVersion = '1.4.0'
+	micrometerTracingVersion = '1.4.1'
 	mockitoVersion = '5.14.2'
 	rabbitmqStreamVersion = '0.18.0'
 	rabbitmqVersion = '5.22.0'

From 4b72f763305843caff3739a0528b60599a0fba3a Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 14 Dec 2024 02:23:47 +0000
Subject: [PATCH 640/737] Bump org.springframework:spring-framework-bom from
 6.2.0 to 6.2.1 (#2928)

Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.2.0 to 6.2.1.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.0...v6.2.1)

---
updated-dependencies:
- dependency-name: org.springframework:spring-framework-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index 587d5e3af2..19e89a49ed 100644
--- a/build.gradle
+++ b/build.gradle
@@ -68,7 +68,7 @@ ext {
 	reactorVersion = '2024.0.1'
 	springDataVersion = '2024.1.0'
 	springRetryVersion = '2.0.11'
-	springVersion = '6.2.0'
+	springVersion = '6.2.1'
 	testcontainersVersion = '1.20.4'
 
 	javaProjects = subprojects - project(':spring-amqp-bom')

From 1c49e579064152ad9730d24387336ccb32357f89 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 14 Dec 2024 02:24:00 +0000
Subject: [PATCH 641/737] Bump org.springframework.data:spring-data-bom from
 2024.1.0 to 2024.1.1 (#2926)

Bumps [org.springframework.data:spring-data-bom](https://github.com/spring-projects/spring-data-bom) from 2024.1.0 to 2024.1.1.
- [Release notes](https://github.com/spring-projects/spring-data-bom/releases)
- [Commits](https://github.com/spring-projects/spring-data-bom/compare/2024.1.0...2024.1.1)

---
updated-dependencies:
- dependency-name: org.springframework.data:spring-data-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index 19e89a49ed..02b67b3adc 100644
--- a/build.gradle
+++ b/build.gradle
@@ -66,7 +66,7 @@ ext {
 	rabbitmqStreamVersion = '0.18.0'
 	rabbitmqVersion = '5.22.0'
 	reactorVersion = '2024.0.1'
-	springDataVersion = '2024.1.0'
+	springDataVersion = '2024.1.1'
 	springRetryVersion = '2.0.11'
 	springVersion = '6.2.1'
 	testcontainersVersion = '1.20.4'

From ede2d209ff7168f32c0d7633c98745e5746b0c71 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 16 Dec 2024 18:40:59 +0000
Subject: [PATCH 642/737] Bump org.junit:junit-bom from 5.11.3 to 5.11.4
 (#2931)

Bumps [org.junit:junit-bom](https://github.com/junit-team/junit5) from 5.11.3 to 5.11.4.
- [Release notes](https://github.com/junit-team/junit5/releases)
- [Commits](https://github.com/junit-team/junit5/compare/r5.11.3...r5.11.4)

---
updated-dependencies:
- dependency-name: org.junit:junit-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index 02b67b3adc..5877efe080 100644
--- a/build.gradle
+++ b/build.gradle
@@ -55,7 +55,7 @@ ext {
 	jacksonBomVersion = '2.18.2'
 	jaywayJsonPathVersion = '2.9.0'
 	junit4Version = '4.13.2'
-	junitJupiterVersion = '5.11.3'
+	junitJupiterVersion = '5.11.4'
 	kotlinCoroutinesVersion = '1.8.1'
 	log4jVersion = '2.24.3'
 	logbackVersion = '1.5.12'

From 085d21ef9d966a0f222061bb3d2c5952ce286f00 Mon Sep 17 00:00:00 2001
From: Spring Builds 
Date: Mon, 16 Dec 2024 19:04:37 +0000
Subject: [PATCH 643/737] [artifactory-release] Release version 3.2.1

---
 gradle.properties | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gradle.properties b/gradle.properties
index 2b07979582..2e0f03694f 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
-version=3.2.1-SNAPSHOT
+version=3.2.1
 org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8
 org.gradle.daemon=true
 org.gradle.caching=true

From d139c981c24cf94f1baa1f50a4ad7958873fab1c Mon Sep 17 00:00:00 2001
From: Spring Builds 
Date: Mon, 16 Dec 2024 19:04:37 +0000
Subject: [PATCH 644/737] [artifactory-release] Next development version

---
 gradle.properties | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gradle.properties b/gradle.properties
index 2e0f03694f..8e33f5cdb5 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
-version=3.2.1
+version=3.2.2-SNAPSHOT
 org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8
 org.gradle.daemon=true
 org.gradle.caching=true

From 7ea51de809921bc67f01f6ee2b9e33a04a20f763 Mon Sep 17 00:00:00 2001
From: Artem Bilan 
Date: Thu, 19 Dec 2024 10:32:27 -0500
Subject: [PATCH 645/737] GH-2933: Fix deprecation in the
 `RestTemplateNodeLocator`

Fixes: https://github.com/spring-projects/spring-amqp/issues/2933

* Remove bogus `RestTemplateHolder` class which is not `public`
and exposed accidentally by the public `RestTemplateNodeLocator`
* Rework `RestTemplateNodeLocator` logic to expose `RestTemplate` directly.
* Populated `AuthCache` for `HttpHost` based on the `baseUri` on-demand
---
 .../rabbit/connection/RestTemplateHolder.java | 41 -------------
 .../connection/RestTemplateNodeLocator.java   | 60 +++++++++----------
 2 files changed, 29 insertions(+), 72 deletions(-)
 delete mode 100644 spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateHolder.java

diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateHolder.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateHolder.java
deleted file mode 100644
index 056435925c..0000000000
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateHolder.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * 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.connection;
-
-import org.springframework.web.client.RestTemplate;
-
-/**
- * Holder for a {@link RestTemplate} and credentials.
- *
- * @author Gary Russell
- * @since 2.4.8
- *
- */
-class RestTemplateHolder {
-
-	final String userName; // NOSONAR
-
-	final String password; // NOSONAR
-
-	RestTemplate template; // NOSONAR
-
-	RestTemplateHolder(String userName, String password) {
-		this.userName = userName;
-		this.password = password;
-	}
-
-}
diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateNodeLocator.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateNodeLocator.java
index f736ae6f58..dacefb649f 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateNodeLocator.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateNodeLocator.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022 the original author or authors.
+ * Copyright 2022-2024 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.
@@ -17,18 +17,17 @@
 package org.springframework.amqp.rabbit.connection;
 
 import java.net.URI;
-import java.net.URISyntaxException;
 import java.nio.charset.StandardCharsets;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.apache.hc.client5.http.auth.AuthCache;
 import org.apache.hc.client5.http.impl.auth.BasicAuthCache;
 import org.apache.hc.client5.http.impl.auth.BasicScheme;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
 import org.apache.hc.core5.http.HttpHost;
-import org.apache.hc.core5.http.protocol.BasicHttpContext;
-import org.apache.hc.core5.http.protocol.HttpContext;
 
+import org.springframework.core.ParameterizedTypeReference;
 import org.springframework.http.HttpMethod;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
@@ -42,44 +41,43 @@
  * A {@link NodeLocator} using the {@link RestTemplate}.
  *
  * @author Gary Russell
+ * @author Artem Bilan
+ *
  * @since 3.0
  *
  */
-public class RestTemplateNodeLocator implements NodeLocator {
+public class RestTemplateNodeLocator implements NodeLocator {
+
+	private final AuthCache authCache = new BasicAuthCache();
+
+	private final AtomicBoolean authSchemeIsSetToCache = new AtomicBoolean(false);
 
 	@Override
-	public RestTemplateHolder createClient(String userName, String password) {
-		return new RestTemplateHolder(userName, password);
+	public RestTemplate createClient(String userName, String password) {
+		HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
+		requestFactory.setHttpContextFactory((httpMethod, uri) -> {
+			HttpClientContext context = HttpClientContext.create();
+			context.setAuthCache(this.authCache);
+			return context;
+		});
+		RestTemplate template = new RestTemplate(requestFactory);
+		template.getInterceptors().add(new BasicAuthenticationInterceptor(userName, password));
+		return template;
 	}
 
-	@SuppressWarnings({ "unchecked", "rawtypes" })
 	@Override
 	@Nullable
-	public Map restCall(RestTemplateHolder client, String baseUri, String vhost, String queue)
-			throws URISyntaxException {
-
-		if (client.template == null) {
-			URI uri = new URI(baseUri);
-			HttpHost host = new HttpHost(uri.getHost(), uri.getPort());
-			client.template = new RestTemplate(new HttpComponentsClientHttpRequestFactory() {
-
-				@Override
-				@Nullable
-				protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri) {
-					AuthCache cache = new BasicAuthCache();
-					BasicScheme scheme = new BasicScheme();
-					cache.put(host, scheme);
-					BasicHttpContext context = new BasicHttpContext();
-					context.setAttribute(HttpClientContext.AUTH_CACHE, cache);
-					return context;
-				}
-
-			});
-			client.template.getInterceptors().add(new BasicAuthenticationInterceptor(client.userName, client.password));
+	public Map restCall(RestTemplate client, String baseUri, String vhost, String queue) {
+		URI theBaseUri = URI.create(baseUri);
+		if (!this.authSchemeIsSetToCache.getAndSet(true)) {
+			this.authCache.put(HttpHost.create(theBaseUri), new BasicScheme());
 		}
-		URI uri = new URI(baseUri)
+		URI uri = theBaseUri
 				.resolve("/api/queues/" + UriUtils.encodePathSegment(vhost, StandardCharsets.UTF_8) + "/" + queue);
-		ResponseEntity response = client.template.exchange(uri, HttpMethod.GET, null, Map.class);
+		ResponseEntity> response =
+				client.exchange(uri, HttpMethod.GET, null, new ParameterizedTypeReference<>() {
+
+				});
 		return response.getStatusCode().equals(HttpStatus.OK) ? response.getBody() : null;
 	}
 

From 410b584e5ccfb5eace001e3d6a682f9945098ac3 Mon Sep 17 00:00:00 2001
From: BenEfrati 
Date: Thu, 19 Dec 2024 23:58:46 +0200
Subject: [PATCH 646/737] GH-2478: Handle conversion exception in
 AsyncRabbitTemplate

Fixes: https://github.com/spring-projects/spring-amqp/issues/2478

Previously, conversion errors in `AsyncRabbitTemplate` have led to `AmqpReplyTimeoutException`

* Fix `AsyncRabbitTemplate` to catch conversion errors on receiving reply and call `rabbitFuture.completeExceptionally()`, respectively.
* Use AssertJ `CompletableFuture` assertions for exception verification
---
 .../amqp/rabbit/AsyncRabbitTemplate.java      |  11 +-
 .../amqp/rabbit/AsyncRabbitTemplateTests.java | 156 ++++++++++--------
 2 files changed, 95 insertions(+), 72 deletions(-)

diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java
index 5bc5f44c89..4a48611742 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java
@@ -49,6 +49,7 @@
 import org.springframework.amqp.rabbit.listener.DirectReplyToMessageListenerContainer.ChannelHolder;
 import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
 import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
+import org.springframework.amqp.support.converter.MessageConversionException;
 import org.springframework.amqp.support.converter.MessageConverter;
 import org.springframework.amqp.support.converter.SmartMessageConverter;
 import org.springframework.amqp.utils.JavaUtils;
@@ -89,6 +90,7 @@
  * @author Artem Bilan
  * @author FengYang Su
  * @author Ngoc Nhan
+ * @author Ben Efrati
  *
  * @since 1.6
  */
@@ -604,12 +606,17 @@ public void onMessage(Message message, Channel channel) {
 					if (future instanceof RabbitConverterFuture) {
 						MessageConverter messageConverter = this.template.getMessageConverter();
 						RabbitConverterFuture rabbitFuture = (RabbitConverterFuture) future;
-						Object converted = rabbitFuture.getReturnType() != null
+						try {
+							Object converted = rabbitFuture.getReturnType() != null
 								&& messageConverter instanceof SmartMessageConverter smart
 								? smart.fromMessage(message,
 								rabbitFuture.getReturnType())
 								: messageConverter.fromMessage(message);
-						rabbitFuture.complete(converted);
+							rabbitFuture.complete(converted);
+						}
+						catch (MessageConversionException e) {
+							rabbitFuture.completeExceptionally(e);
+						}
 					}
 					else {
 						((RabbitMessageFuture) future).complete(message);
diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java
index b0cb6d92c1..153445d1b7 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java
@@ -18,7 +18,6 @@
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
-import static org.assertj.core.api.Assertions.fail;
 import static org.awaitility.Awaitility.await;
 import static org.mockito.Mockito.mock;
 
@@ -57,6 +56,7 @@
 import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
 import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter;
 import org.springframework.amqp.rabbit.listener.adapter.ReplyingMessageListener;
+import org.springframework.amqp.support.converter.MessageConversionException;
 import org.springframework.amqp.support.converter.SimpleMessageConverter;
 import org.springframework.amqp.support.postprocessor.GUnzipPostProcessor;
 import org.springframework.amqp.support.postprocessor.GZipPostProcessor;
@@ -72,6 +72,7 @@
 /**
  * @author Gary Russell
  * @author Artem Bilan
+ * @author Ben Efrati
  *
  * @since 1.6
  */
@@ -100,7 +101,7 @@ static void setup() {
 	}
 
 	@Test
-	public void testConvert1Arg() throws Exception {
+	public void testConvert1Arg()  {
 		final AtomicBoolean mppCalled = new AtomicBoolean();
 		CompletableFuture future = this.asyncTemplate.convertSendAndReceive("foo", m -> {
 			mppCalled.set(true);
@@ -111,7 +112,7 @@ public void testConvert1Arg() throws Exception {
 	}
 
 	@Test
-	public void testConvert1ArgDirect() throws Exception {
+	public void testConvert1ArgDirect() {
 		this.latch.set(new CountDownLatch(1));
 		CompletableFuture future1 = this.asyncDirectTemplate.convertSendAndReceive("foo");
 		CompletableFuture future2 = this.asyncDirectTemplate.convertSendAndReceive("bar");
@@ -139,19 +140,19 @@ public void testConvert1ArgDirect() throws Exception {
 	}
 
 	@Test
-	public void testConvert2Args() throws Exception {
+	public void testConvert2Args() {
 		CompletableFuture future = this.asyncTemplate.convertSendAndReceive(this.requests.getName(), "foo");
 		checkConverterResult(future, "FOO");
 	}
 
 	@Test
-	public void testConvert3Args() throws Exception {
+	public void testConvert3Args() {
 		CompletableFuture future = this.asyncTemplate.convertSendAndReceive("", this.requests.getName(), "foo");
 		checkConverterResult(future, "FOO");
 	}
 
 	@Test
-	public void testConvert4Args() throws Exception {
+	public void testConvert4Args() {
 		CompletableFuture future = this.asyncTemplate.convertSendAndReceive("", this.requests.getName(), "foo",
 				message -> {
 					String body = new String(message.getBody());
@@ -187,7 +188,7 @@ public void testMessage1ArgDirect() throws Exception {
 		assertThat(TestUtils
 				.getPropertyValue(this.asyncDirectTemplate, "directReplyToContainer.consumerCount",
 						AtomicInteger.class).get())
-				.isEqualTo(0);
+				.isZero();
 	}
 
 	private void waitForZeroInUseConsumers() {
@@ -214,7 +215,7 @@ public void testMessage3Args() throws Exception {
 	public void testCancel() {
 		CompletableFuture future = this.asyncTemplate.convertSendAndReceive("foo");
 		future.cancel(false);
-		assertThat(TestUtils.getPropertyValue(asyncTemplate, "pending", Map.class)).hasSize(0);
+		assertThat(TestUtils.getPropertyValue(asyncTemplate, "pending", Map.class)).isEmpty();
 	}
 
 	@Test
@@ -234,44 +235,46 @@ private Message getFooMessage() {
 
 	@Test
 	@DirtiesContext
-	public void testReturn() throws Exception {
+	public void testReturn() {
 		this.asyncTemplate.setMandatory(true);
 		CompletableFuture future = this.asyncTemplate.convertSendAndReceive(this.requests.getName() + "x",
 				"foo");
-		try {
-			future.get(10, TimeUnit.SECONDS);
-			fail("Expected exception");
-		}
-		catch (ExecutionException e) {
-			assertThat(e.getCause()).isInstanceOf(AmqpMessageReturnedException.class);
-			assertThat(((AmqpMessageReturnedException) e.getCause()).getRoutingKey()).isEqualTo(this.requests.getName() + "x");
-		}
+		assertThat(future)
+				.as("Expected exception")
+				.failsWithin(Duration.ofSeconds(10))
+				.withThrowableOfType(ExecutionException.class)
+				.havingCause()
+				.isInstanceOf(AmqpMessageReturnedException.class)
+				.extracting(cause -> ((AmqpMessageReturnedException) cause).getRoutingKey())
+				.isEqualTo(this.requests.getName() + "x");
 	}
 
 	@Test
 	@DirtiesContext
-	public void testReturnDirect() throws Exception {
+	public void testReturnDirect() {
 		this.asyncDirectTemplate.setMandatory(true);
 		CompletableFuture future = this.asyncDirectTemplate.convertSendAndReceive(this.requests.getName() + "x",
 				"foo");
-		try {
-			future.get(10, TimeUnit.SECONDS);
-			fail("Expected exception");
-		}
-		catch (ExecutionException e) {
-			assertThat(e.getCause()).isInstanceOf(AmqpMessageReturnedException.class);
-			assertThat(((AmqpMessageReturnedException) e.getCause()).getRoutingKey()).isEqualTo(this.requests.getName() + "x");
-		}
+
+		assertThat(future)
+				.as("Expected exception")
+				.failsWithin(Duration.ofSeconds(10))
+				.withThrowableOfType(ExecutionException.class)
+				.havingCause()
+				.isInstanceOf(AmqpMessageReturnedException.class)
+				.extracting(cause -> ((AmqpMessageReturnedException) cause).getRoutingKey())
+				.isEqualTo(this.requests.getName() + "x");
 	}
 
 	@Test
 	@DirtiesContext
-	public void testConvertWithConfirm() throws Exception {
+	public void testConvertWithConfirm() {
 		this.asyncTemplate.setEnableConfirms(true);
 		RabbitConverterFuture future = this.asyncTemplate.convertSendAndReceive("sleep");
 		CompletableFuture confirm = future.getConfirm();
-		assertThat(confirm).isNotNull();
-		assertThat(confirm.get(10, TimeUnit.SECONDS)).isTrue();
+		assertThat(confirm).isNotNull()
+				.succeedsWithin(Duration.ofSeconds(10))
+				.isEqualTo(true);
 		checkConverterResult(future, "SLEEP");
 	}
 
@@ -282,19 +285,21 @@ public void testMessageWithConfirm() throws Exception {
 		RabbitMessageFuture future = this.asyncTemplate
 				.sendAndReceive(new SimpleMessageConverter().toMessage("sleep", new MessageProperties()));
 		CompletableFuture confirm = future.getConfirm();
-		assertThat(confirm).isNotNull();
-		assertThat(confirm.get(10, TimeUnit.SECONDS)).isTrue();
+		assertThat(confirm).isNotNull()
+				.succeedsWithin(Duration.ofSeconds(10))
+				.isEqualTo(true);
 		checkMessageResult(future, "SLEEP");
 	}
 
 	@Test
 	@DirtiesContext
-	public void testConvertWithConfirmDirect() throws Exception {
+	public void testConvertWithConfirmDirect() {
 		this.asyncDirectTemplate.setEnableConfirms(true);
 		RabbitConverterFuture future = this.asyncDirectTemplate.convertSendAndReceive("sleep");
 		CompletableFuture confirm = future.getConfirm();
-		assertThat(confirm).isNotNull();
-		assertThat(confirm.get(10, TimeUnit.SECONDS)).isTrue();
+		assertThat(confirm).isNotNull()
+				.succeedsWithin(Duration.ofSeconds(10))
+				.isEqualTo(true);
 		checkConverterResult(future, "SLEEP");
 	}
 
@@ -305,8 +310,9 @@ public void testMessageWithConfirmDirect() throws Exception {
 		RabbitMessageFuture future = this.asyncDirectTemplate
 				.sendAndReceive(new SimpleMessageConverter().toMessage("sleep", new MessageProperties()));
 		CompletableFuture confirm = future.getConfirm();
-		assertThat(confirm).isNotNull();
-		assertThat(confirm.get(10, TimeUnit.SECONDS)).isTrue();
+		assertThat(confirm).isNotNull()
+				.succeedsWithin(Duration.ofSeconds(10))
+				.isEqualTo(true);
 		checkMessageResult(future, "SLEEP");
 	}
 
@@ -319,14 +325,12 @@ public void testReceiveTimeout() throws Exception {
 		TheCallback callback = new TheCallback();
 		future.whenComplete(callback);
 		assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class)).hasSize(1);
-		try {
-			future.get(10, TimeUnit.SECONDS);
-			fail("Expected ExecutionException");
-		}
-		catch (ExecutionException e) {
-			assertThat(e.getCause()).isInstanceOf(AmqpReplyTimeoutException.class);
-		}
-		assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class)).hasSize(0);
+		assertThat(future)
+				.as("Expected ExecutionException")
+				.failsWithin(Duration.ofSeconds(10))
+				.withThrowableOfType(ExecutionException.class)
+				.withCauseInstanceOf(AmqpReplyTimeoutException.class);
+		assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class)).isEmpty();
 		assertThat(callback.latch.await(10, TimeUnit.SECONDS)).isTrue();
 		assertThat(callback.ex).isInstanceOf(AmqpReplyTimeoutException.class);
 	}
@@ -340,14 +344,13 @@ public void testReplyAfterReceiveTimeout() throws Exception {
 		TheCallback callback = new TheCallback();
 		future.whenComplete(callback);
 		assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class)).hasSize(1);
-		try {
-			future.get(10, TimeUnit.SECONDS);
-			fail("Expected ExecutionException");
-		}
-		catch (ExecutionException e) {
-			assertThat(e.getCause()).isInstanceOf(AmqpReplyTimeoutException.class);
-		}
-		assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class)).hasSize(0);
+
+		assertThat(future)
+				.as("Expected ExecutionException")
+				.failsWithin(Duration.ofSeconds(10))
+				.withThrowableOfType(ExecutionException.class)
+				.withCauseInstanceOf(AmqpReplyTimeoutException.class);
+		assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class)).isEmpty();
 		assertThat(callback.latch.await(10, TimeUnit.SECONDS)).isTrue();
 		assertThat(callback.ex).isInstanceOf(AmqpReplyTimeoutException.class);
 
@@ -373,16 +376,17 @@ public void testStopCancelled() throws Exception {
 		this.asyncTemplate.stop();
 		// Second stop() to be sure that it is idempotent
 		this.asyncTemplate.stop();
-		try {
-			future.get(10, TimeUnit.SECONDS);
-			fail("Expected CancellationException");
-		}
-		catch (CancellationException e) {
-			assertThat(future.getNackCause()).isEqualTo("AsyncRabbitTemplate was stopped while waiting for reply");
-		}
-		assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class)).hasSize(0);
+		assertThat(future)
+				.as("Expected CancellationException")
+				.failsWithin(Duration.ofSeconds(10))
+				.withThrowableOfType(CancellationException.class)
+				.satisfies(e -> {
+					assertThat(future.getNackCause()).isEqualTo("AsyncRabbitTemplate was stopped while waiting for reply");
+					assertThat(future).isCancelled();
+				});
+
+		assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "pending", Map.class)).isEmpty();
 		assertThat(callback.latch.await(10, TimeUnit.SECONDS)).isTrue();
-		assertThat(future.isCancelled()).isTrue();
 		assertThat(TestUtils.getPropertyValue(this.asyncTemplate, "taskScheduler")).isNull();
 
 		/*
@@ -394,6 +398,23 @@ public void testStopCancelled() throws Exception {
 		assertThat(callback.result).isNull();
 	}
 
+	@Test
+	@DirtiesContext
+	public void testConversionException() {
+		this.asyncTemplate.getRabbitTemplate().setMessageConverter(new SimpleMessageConverter() {
+			@Override
+			public Object fromMessage(Message message) throws MessageConversionException {
+				throw new MessageConversionException("Failed to convert message");
+			}
+		});
+
+		RabbitConverterFuture replyFuture = this.asyncTemplate.convertSendAndReceive("conversionException");
+
+		assertThat(replyFuture).failsWithin(Duration.ofSeconds(10))
+				.withThrowableThat()
+				.withCauseInstanceOf(MessageConversionException.class);
+	}
+
 	@Test
 	void ctorCoverage() {
 		AsyncRabbitTemplate template = new AsyncRabbitTemplate(mock(ConnectionFactory.class), "ex", "rk");
@@ -461,15 +482,10 @@ public void limitedChannelsAreReleasedOnTimeout() {
 		connectionFactory.destroy();
 	}
 
-	private void checkConverterResult(CompletableFuture future, String expected) throws InterruptedException {
-		final CountDownLatch cdl = new CountDownLatch(1);
-		final AtomicReference resultRef = new AtomicReference<>();
-		future.whenComplete((result, ex) -> {
-			resultRef.set(result);
-			cdl.countDown();
-		});
-		assertThat(cdl.await(10, TimeUnit.SECONDS)).isTrue();
-		assertThat(resultRef.get()).isEqualTo(expected);
+	private void checkConverterResult(CompletableFuture future, String expected) {
+		assertThat(future)
+				.succeedsWithin(Duration.ofSeconds(10))
+				.isEqualTo(expected);
 	}
 
 	private Message checkMessageResult(CompletableFuture future, String expected) throws InterruptedException {

From 1fb95a24da105772fb0f5348e63209e879b25f32 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 20 Dec 2024 21:57:57 -0500
Subject: [PATCH 647/737] Bump the development-dependencies group with 2
 updates (#2934)

Bumps the development-dependencies group with 2 updates: io.spring.dependency-management and com.github.spotbugs.


Updates `io.spring.dependency-management` from 1.1.6 to 1.1.7

Updates `com.github.spotbugs` from 6.0.26 to 6.0.27

---
updated-dependencies:
- dependency-name: io.spring.dependency-management
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: com.github.spotbugs
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 build.gradle | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/build.gradle b/build.gradle
index 5877efe080..a8541b8ff2 100644
--- a/build.gradle
+++ b/build.gradle
@@ -20,10 +20,10 @@ plugins {
 	id 'idea'
 	id 'org.ajoberstar.grgit' version '5.3.0'
 	id 'io.spring.nohttp' version '0.0.11'
-	id 'io.spring.dependency-management' version '1.1.6' apply false
+	id 'io.spring.dependency-management' version '1.1.7' apply false
 	id 'org.antora' version '1.0.0'
 	id 'io.spring.antora.generate-antora-yml' version '0.0.1'
-	id 'com.github.spotbugs' version '6.0.26'
+	id 'com.github.spotbugs' version '6.0.27'
 	id 'io.freefair.aggregate-javadoc' version '8.10.2'
 }
 

From db0c274d15717b9eef6da236a5f249e6ac053b99 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 20 Dec 2024 21:58:17 -0500
Subject: [PATCH 648/737] Bump ch.qos.logback:logback-classic from 1.5.12 to
 1.5.14 (#2935)

Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.12 to 1.5.14.
- [Commits](https://github.com/qos-ch/logback/compare/v_1.5.12...v_1.5.14)

---
updated-dependencies:
- dependency-name: ch.qos.logback:logback-classic
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index a8541b8ff2..6daaeddbe1 100644
--- a/build.gradle
+++ b/build.gradle
@@ -58,7 +58,7 @@ ext {
 	junitJupiterVersion = '5.11.4'
 	kotlinCoroutinesVersion = '1.8.1'
 	log4jVersion = '2.24.3'
-	logbackVersion = '1.5.12'
+	logbackVersion = '1.5.14'
 	micrometerDocsVersion = '1.0.4'
 	micrometerVersion = '1.14.2'
 	micrometerTracingVersion = '1.4.1'

From 75a05d72fb70de30fd39dbdc1cf8c2f6e1c2080a Mon Sep 17 00:00:00 2001
From: Artem Bilan 
Date: Mon, 23 Dec 2024 10:18:13 -0500
Subject: [PATCH 649/737] Fix race condition in the
 RabbitTemplatePublisherCallbacksIntegration3Tests

The `CachingConnectionFactory` has an ability to wait for async channels close when it is destroyed.
However, that happens only if an `ApplicationContext` is stopped.

* Supply a mock `ApplicationContext` to the `CachingConnectionFactory` of the test
and emit respective `ContextClosedEvent` in the test before calling `cf.destroy()`
---
 ...tePublisherCallbacksIntegration3Tests.java | 20 +++++++++++++------
 1 file changed, 14 insertions(+), 6 deletions(-)

diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration3Tests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration3Tests.java
index d9f1dddd47..1c489a7dbc 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration3Tests.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration3Tests.java
@@ -17,6 +17,7 @@
 package org.springframework.amqp.rabbit.core;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
 
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
@@ -33,11 +34,15 @@
 import org.springframework.amqp.rabbit.junit.RabbitAvailable;
 import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition;
 import org.springframework.amqp.utils.test.TestUtils;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.event.ContextClosedEvent;
 
 import com.rabbitmq.client.Channel;
 
 /**
  * @author Gary Russell
+ * @author Artem Bilan
+ *
  * @since 2.1
  *
  */
@@ -72,15 +77,17 @@ public void testRepublishOnNackThreadNoExchange() throws Exception {
 
 	@Test
 	public void testDeferredChannelCacheNack() throws Exception {
-		final CachingConnectionFactory cf = new CachingConnectionFactory(
+		CachingConnectionFactory cf = new CachingConnectionFactory(
 				RabbitAvailableCondition.getBrokerRunning().getConnectionFactory());
 		cf.setPublisherReturns(true);
 		cf.setPublisherConfirmType(ConfirmType.CORRELATED);
-		final RabbitTemplate template = new RabbitTemplate(cf);
-		final CountDownLatch returnLatch = new CountDownLatch(1);
-		final CountDownLatch confirmLatch = new CountDownLatch(1);
-		final AtomicInteger cacheCount = new AtomicInteger();
-		final AtomicBoolean returnCalledFirst = new AtomicBoolean();
+		ApplicationContext mockApplicationContext = mock();
+		cf.setApplicationContext(mockApplicationContext);
+		RabbitTemplate template = new RabbitTemplate(cf);
+		CountDownLatch returnLatch = new CountDownLatch(1);
+		CountDownLatch confirmLatch = new CountDownLatch(1);
+		AtomicInteger cacheCount = new AtomicInteger();
+		AtomicBoolean returnCalledFirst = new AtomicBoolean();
 		template.setConfirmCallback((cd, a, c) -> {
 			cacheCount.set(TestUtils.getPropertyValue(cf, "cachedChannelsNonTransactional", List.class).size());
 			returnCalledFirst.set(returnLatch.getCount() == 0);
@@ -104,6 +111,7 @@ public void testDeferredChannelCacheNack() throws Exception {
 		assertThat(cacheCount.get()).isEqualTo(1);
 		assertThat(returnCalledFirst.get()).isTrue();
 		assertThat(correlationData.getReturned()).isNotNull();
+		cf.onApplicationEvent(new ContextClosedEvent(mockApplicationContext));
 		cf.destroy();
 	}
 

From f9fd9294a7a958098b6b6e06e499dd6b288262bb Mon Sep 17 00:00:00 2001
From: BenEfrati 
Date: Mon, 23 Dec 2024 23:07:17 +0200
Subject: [PATCH 650/737] Fix containerFactory SpEL Resolution

Related to: https://github.com/spring-projects/spring-amqp/issues/2809
---
 ...itListenerAnnotationBeanPostProcessor.java |  2 +-
 .../annotation/MockMultiRabbitTests.java      | 54 ++++++++++++++++---
 2 files changed, 49 insertions(+), 7 deletions(-)

diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/MultiRabbitListenerAnnotationBeanPostProcessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/MultiRabbitListenerAnnotationBeanPostProcessor.java
index 0764691e1e..521adc21e5 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/MultiRabbitListenerAnnotationBeanPostProcessor.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/MultiRabbitListenerAnnotationBeanPostProcessor.java
@@ -95,7 +95,7 @@ protected String resolveMultiRabbitAdminName(RabbitListener rabbitListener) {
 				return rlcf.getBeanName() + RabbitListenerConfigUtils.MULTI_RABBIT_ADMIN_SUFFIX;
 			}
 
-			return containerFactory + RabbitListenerConfigUtils.MULTI_RABBIT_ADMIN_SUFFIX;
+			return resolved + RabbitListenerConfigUtils.MULTI_RABBIT_ADMIN_SUFFIX;
 		}
 
 		return RabbitListenerConfigUtils.RABBIT_ADMIN_BEAN_NAME;
diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MockMultiRabbitTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MockMultiRabbitTests.java
index cda0362fe2..22956228d2 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MockMultiRabbitTests.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MockMultiRabbitTests.java
@@ -68,7 +68,7 @@ void multipleSimpleMessageListeners() {
 
 		Map factories = context
 				.getBeansOfType(RabbitListenerContainerTestFactory.class, false, false);
-		Assertions.assertThat(factories).hasSize(3);
+		Assertions.assertThat(factories).hasSize(4);
 
 		factories.values().forEach(factory -> {
 			Assertions.assertThat(factory.getListenerContainers().size())
@@ -99,34 +99,38 @@ void testDeclarablesMatchProperRabbitAdmin() {
 
 		Map factories = context
 				.getBeansOfType(RabbitListenerContainerTestFactory.class, false, false);
-		Assertions.assertThat(factories).hasSize(3);
+		Assertions.assertThat(factories).hasSize(4);
 
 		BiFunction declares = (admin, dec) -> dec.getDeclaringAdmins().size() == 1
 				&& dec.getDeclaringAdmins().contains(admin.getBeanName());
 
 		Map exchanges = context.getBeansOfType(AbstractExchange.class, false, false)
 				.values().stream().collect(Collectors.toMap(AbstractExchange::getName, v -> v));
-		Assertions.assertThat(exchanges).hasSize(3);
+		Assertions.assertThat(exchanges).hasSize(4);
 		Assertions.assertThat(declares.apply(MultiConfig.DEFAULT_RABBIT_ADMIN, exchanges.get("testExchange"))).isTrue();
 		Assertions.assertThat(declares.apply(MultiConfig.RABBIT_ADMIN_BROKER_B, exchanges.get("testExchangeB")))
 				.isTrue();
 		Assertions.assertThat(declares.apply(MultiConfig.RABBIT_ADMIN_BROKER_C, exchanges.get("testExchangeC")))
 				.isTrue();
+		Assertions.assertThat(declares.apply(MultiConfig.RABBIT_ADMIN_BROKER_D, exchanges.get("testExchangeD")))
+				.isTrue();
 
 		Map queues = context
 				.getBeansOfType(org.springframework.amqp.core.Queue.class, false, false)
 				.values().stream().collect(Collectors.toMap(org.springframework.amqp.core.Queue::getName, v -> v));
-		Assertions.assertThat(queues).hasSize(3);
+		Assertions.assertThat(queues).hasSize(4);
 		Assertions.assertThat(declares.apply(MultiConfig.DEFAULT_RABBIT_ADMIN, queues.get("testQueue"))).isTrue();
 		Assertions.assertThat(declares.apply(MultiConfig.RABBIT_ADMIN_BROKER_B, queues.get("testQueueB"))).isTrue();
 		Assertions.assertThat(declares.apply(MultiConfig.RABBIT_ADMIN_BROKER_C, queues.get("testQueueC"))).isTrue();
+		Assertions.assertThat(declares.apply(MultiConfig.RABBIT_ADMIN_BROKER_D, queues.get("testQueueD"))).isTrue();
 
 		Map bindings = context.getBeansOfType(Binding.class, false, false)
 				.values().stream().collect(Collectors.toMap(Binding::getRoutingKey, v -> v));
-		Assertions.assertThat(bindings).hasSize(3);
+		Assertions.assertThat(bindings).hasSize(4);
 		Assertions.assertThat(declares.apply(MultiConfig.DEFAULT_RABBIT_ADMIN, bindings.get("testKey"))).isTrue();
 		Assertions.assertThat(declares.apply(MultiConfig.RABBIT_ADMIN_BROKER_B, bindings.get("testKeyB"))).isTrue();
 		Assertions.assertThat(declares.apply(MultiConfig.RABBIT_ADMIN_BROKER_C, bindings.get("testKeyC"))).isTrue();
+		Assertions.assertThat(declares.apply(MultiConfig.RABBIT_ADMIN_BROKER_D, bindings.get("testKeyD"))).isTrue();
 
 		context.close(); // Close and stop the listeners
 	}
@@ -180,9 +184,19 @@ void testCreationOfConnections() {
 		Mockito.verify(MultiConfig.CONNECTION_FACTORY_BROKER_C).createConnection();
 		Mockito.verify(MultiConfig.CONNECTION_BROKER_C).createChannel(false);
 
+		Mockito.verify(MultiConfig.CONNECTION_FACTORY_BROKER_D, Mockito.never()).createConnection();
+		Mockito.verify(MultiConfig.CONNECTION_BROKER_D, Mockito.never()).createChannel(false);
+		SimpleResourceHolder.bind(MultiConfig.ROUTING_CONNECTION_FACTORY, "brokerD");
+		rabbitTemplate.convertAndSend("messageToBrokerD");
+		SimpleResourceHolder.unbind(MultiConfig.ROUTING_CONNECTION_FACTORY);
+		Mockito.verify(MultiConfig.CONNECTION_FACTORY_BROKER_D).createConnection();
+		Mockito.verify(MultiConfig.CONNECTION_BROKER_D).createChannel(false);
+
 		context.close(); // Close and stop the listeners
 	}
 
+
+
 	@Test
 	@DisplayName("Test assignment of RabbitAdmin in the endpoint registry")
 	void testAssignmentOfRabbitAdminInTheEndpointRegistry() {
@@ -192,7 +206,7 @@ void testAssignmentOfRabbitAdminInTheEndpointRegistry() {
 		final RabbitListenerEndpointRegistry registry = context.getBean(RabbitListenerEndpointRegistry.class);
 		final Collection listenerContainers = registry.getListenerContainers();
 
-		Assertions.assertThat(listenerContainers).hasSize(3);
+		Assertions.assertThat(listenerContainers).hasSize(4);
 		listenerContainers.forEach(container -> {
 			Assertions.assertThat(container).isInstanceOf(MessageListenerTestContainer.class);
 			final MessageListenerTestContainer refContainer = (MessageListenerTestContainer) container;
@@ -228,6 +242,13 @@ public void handleItB(String body) {
 				key = "testKeyC"))
 		public void handleItC(String body) {
 		}
+
+		@RabbitListener(containerFactory = "${broker-name:brokerD}", bindings = @QueueBinding(
+				exchange = @Exchange("testExchangeD"),
+				value = @Queue("testQueueD"),
+				key = "testKeyD"))
+		public void handleItD(String body) {
+		}
 	}
 
 	@Component
@@ -244,6 +265,10 @@ public void handleItB(String body) {
 		@RabbitListener(queues = "testQueueC", containerFactory = "brokerC")
 		public void handleItC(String body) {
 		}
+
+		@RabbitListener(queues = "testQueueD", containerFactory = "${broker-name:brokerD}")
+		public void handleItD(String body) {
+		}
 	}
 
 	@Configuration
@@ -254,34 +279,41 @@ static class MultiConfig {
 		static final ConnectionFactory DEFAULT_CONNECTION_FACTORY = Mockito.mock(ConnectionFactory.class);
 		static final ConnectionFactory CONNECTION_FACTORY_BROKER_B = Mockito.mock(ConnectionFactory.class);
 		static final ConnectionFactory CONNECTION_FACTORY_BROKER_C = Mockito.mock(ConnectionFactory.class);
+		static final ConnectionFactory CONNECTION_FACTORY_BROKER_D = Mockito.mock(ConnectionFactory.class);
 
 		static final Connection DEFAULT_CONNECTION = Mockito.mock(Connection.class);
 		static final Connection CONNECTION_BROKER_B = Mockito.mock(Connection.class);
 		static final Connection CONNECTION_BROKER_C = Mockito.mock(Connection.class);
+		static final Connection CONNECTION_BROKER_D = Mockito.mock(Connection.class);
 
 		static final Channel DEFAULT_CHANNEL = Mockito.mock(Channel.class);
 		static final Channel CHANNEL_BROKER_B = Mockito.mock(Channel.class);
 		static final Channel CHANNEL_BROKER_C = Mockito.mock(Channel.class);
+		static final Channel CHANNEL_BROKER_D = Mockito.mock(Channel.class);
 
 		static {
 			final Map targetConnectionFactories = new HashMap<>();
 			targetConnectionFactories.put("brokerB", CONNECTION_FACTORY_BROKER_B);
 			targetConnectionFactories.put("brokerC", CONNECTION_FACTORY_BROKER_C);
+			targetConnectionFactories.put("brokerD", CONNECTION_FACTORY_BROKER_D);
 			ROUTING_CONNECTION_FACTORY.setDefaultTargetConnectionFactory(DEFAULT_CONNECTION_FACTORY);
 			ROUTING_CONNECTION_FACTORY.setTargetConnectionFactories(targetConnectionFactories);
 
 			Mockito.when(DEFAULT_CONNECTION_FACTORY.createConnection()).thenReturn(DEFAULT_CONNECTION);
 			Mockito.when(CONNECTION_FACTORY_BROKER_B.createConnection()).thenReturn(CONNECTION_BROKER_B);
 			Mockito.when(CONNECTION_FACTORY_BROKER_C.createConnection()).thenReturn(CONNECTION_BROKER_C);
+			Mockito.when(CONNECTION_FACTORY_BROKER_D.createConnection()).thenReturn(CONNECTION_BROKER_D);
 
 			Mockito.when(DEFAULT_CONNECTION.createChannel(false)).thenReturn(DEFAULT_CHANNEL);
 			Mockito.when(CONNECTION_BROKER_B.createChannel(false)).thenReturn(CHANNEL_BROKER_B);
 			Mockito.when(CONNECTION_BROKER_C.createChannel(false)).thenReturn(CHANNEL_BROKER_C);
+			Mockito.when(CONNECTION_BROKER_D.createChannel(false)).thenReturn(CHANNEL_BROKER_D);
 		}
 
 		static final RabbitAdmin DEFAULT_RABBIT_ADMIN = new RabbitAdmin(DEFAULT_CONNECTION_FACTORY);
 		static final RabbitAdmin RABBIT_ADMIN_BROKER_B = new RabbitAdmin(CONNECTION_FACTORY_BROKER_B);
 		static final RabbitAdmin RABBIT_ADMIN_BROKER_C = new RabbitAdmin(CONNECTION_FACTORY_BROKER_C);
+		static final RabbitAdmin RABBIT_ADMIN_BROKER_D = new RabbitAdmin(CONNECTION_FACTORY_BROKER_D);
 
 		@Bean
 		public RabbitListenerAnnotationBeanPostProcessor postProcessor() {
@@ -307,6 +339,11 @@ public RabbitAdmin rabbitAdminBrokerC() {
 			return RABBIT_ADMIN_BROKER_C;
 		}
 
+		@Bean("brokerD-admin")
+		public RabbitAdmin rabbitAdminBrokerD() {
+			return RABBIT_ADMIN_BROKER_D;
+		}
+
 		@Bean("defaultContainerFactory")
 		public RabbitListenerContainerTestFactory defaultContainerFactory() {
 			return new RabbitListenerContainerTestFactory();
@@ -322,6 +359,11 @@ public RabbitListenerContainerTestFactory containerFactoryBrokerC() {
 			return new RabbitListenerContainerTestFactory();
 		}
 
+		@Bean("brokerD")
+		public RabbitListenerContainerTestFactory containerFactoryBrokerD() {
+			return new RabbitListenerContainerTestFactory();
+		}
+
 		@Bean
 		public RabbitListenerEndpointRegistry rabbitListenerEndpointRegistry() {
 			return new RabbitListenerEndpointRegistry();

From e710f24a08e324522dce9d123666d9438d4a5eb3 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 28 Dec 2024 02:15:44 +0000
Subject: [PATCH 651/737] Bump ch.qos.logback:logback-classic from 1.5.14 to
 1.5.15 (#2937)

Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.14 to 1.5.15.
- [Commits](https://github.com/qos-ch/logback/compare/v_1.5.14...v_1.5.15)

---
updated-dependencies:
- dependency-name: ch.qos.logback:logback-classic
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index 6daaeddbe1..386f9c9f9d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -58,7 +58,7 @@ ext {
 	junitJupiterVersion = '5.11.4'
 	kotlinCoroutinesVersion = '1.8.1'
 	log4jVersion = '2.24.3'
-	logbackVersion = '1.5.14'
+	logbackVersion = '1.5.15'
 	micrometerDocsVersion = '1.0.4'
 	micrometerVersion = '1.14.2'
 	micrometerTracingVersion = '1.4.1'

From f8813e2a014d481e553ce0bdf7b1ed7d4f919b02 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 11 Jan 2025 02:09:56 +0000
Subject: [PATCH 652/737] Bump com.github.spotbugs in the
 development-dependencies group (#2939)

Bumps the development-dependencies group with 1 update: com.github.spotbugs.


Updates `com.github.spotbugs` from 6.0.27 to 6.0.28

---
updated-dependencies:
- dependency-name: com.github.spotbugs
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index 386f9c9f9d..523e979479 100644
--- a/build.gradle
+++ b/build.gradle
@@ -23,7 +23,7 @@ plugins {
 	id 'io.spring.dependency-management' version '1.1.7' apply false
 	id 'org.antora' version '1.0.0'
 	id 'io.spring.antora.generate-antora-yml' version '0.0.1'
-	id 'com.github.spotbugs' version '6.0.27'
+	id 'com.github.spotbugs' version '6.0.28'
 	id 'io.freefair.aggregate-javadoc' version '8.10.2'
 }
 

From 3a07153f04136ade9f01d121bd7e7eec5c6a7544 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 11 Jan 2025 02:11:27 +0000
Subject: [PATCH 653/737] Bump ch.qos.logback:logback-classic from 1.5.15 to
 1.5.16 (#2940)

Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.15 to 1.5.16.
- [Commits](https://github.com/qos-ch/logback/compare/v_1.5.15...v_1.5.16)

---
updated-dependencies:
- dependency-name: ch.qos.logback:logback-classic
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index 523e979479..280e070d79 100644
--- a/build.gradle
+++ b/build.gradle
@@ -58,7 +58,7 @@ ext {
 	junitJupiterVersion = '5.11.4'
 	kotlinCoroutinesVersion = '1.8.1'
 	log4jVersion = '2.24.3'
-	logbackVersion = '1.5.15'
+	logbackVersion = '1.5.16'
 	micrometerDocsVersion = '1.0.4'
 	micrometerVersion = '1.14.2'
 	micrometerTracingVersion = '1.4.1'

From 0752c32a4e082897ef8fb68f25fffa3eaeefce5b Mon Sep 17 00:00:00 2001
From: Artem Bilan 
Date: Tue, 14 Jan 2025 11:59:52 -0500
Subject: [PATCH 654/737] Fix race condition in
 RabbitTemplatePublisherCallbacksIntegration1Tests

The `CachingConnectionFactory` has an ability to wait for async channels close when it is destroyed.
However, that happens only if an `ApplicationContext` is stopped.

* Supply a mock `ApplicationContext` to the `CachingConnectionFactory` of the test
and emit respective `ContextClosedEvent` in the test before calling `cf.destroy()`
---
 ...tePublisherCallbacksIntegration1Tests.java | 34 ++++++++++++++++---
 1 file changed, 29 insertions(+), 5 deletions(-)

diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration1Tests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration1Tests.java
index fef647063b..1553c12c4d 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration1Tests.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration1Tests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -79,6 +79,8 @@
 import org.springframework.amqp.support.converter.SimpleMessageConverter;
 import org.springframework.amqp.utils.test.TestUtils;
 import org.springframework.beans.DirectFieldAccessor;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.event.ContextClosedEvent;
 import org.springframework.expression.Expression;
 import org.springframework.expression.spel.standard.SpelExpressionParser;
 
@@ -100,6 +102,8 @@ public class RabbitTemplatePublisherCallbacksIntegration1Tests {
 
 	public static final String ROUTE = "test.queue.RabbitTemplatePublisherCallbacksIntegrationTests";
 
+	private static final ApplicationContext APPLICATION_CONTEXT = mock();
+
 	private final ExecutorService executorService = Executors.newSingleThreadExecutor();
 
 	private CachingConnectionFactory connectionFactory;
@@ -122,25 +126,35 @@ public void create() {
 		connectionFactory.setHost("localhost");
 		connectionFactory.setChannelCacheSize(10);
 		connectionFactory.setPort(BrokerTestUtils.getPort());
+		connectionFactory.setApplicationContext(APPLICATION_CONTEXT);
+
 		connectionFactoryWithConfirmsEnabled = new CachingConnectionFactory();
 		connectionFactoryWithConfirmsEnabled.setHost("localhost");
 		connectionFactoryWithConfirmsEnabled.setChannelCacheSize(100);
 		connectionFactoryWithConfirmsEnabled.setPort(BrokerTestUtils.getPort());
 		connectionFactoryWithConfirmsEnabled.setPublisherConfirmType(ConfirmType.CORRELATED);
+		connectionFactoryWithConfirmsEnabled.setApplicationContext(APPLICATION_CONTEXT);
+
 		templateWithConfirmsEnabled = new RabbitTemplate(connectionFactoryWithConfirmsEnabled);
+
 		connectionFactoryWithReturnsEnabled = new CachingConnectionFactory();
 		connectionFactoryWithReturnsEnabled.setHost("localhost");
 		connectionFactoryWithReturnsEnabled.setChannelCacheSize(1);
 		connectionFactoryWithReturnsEnabled.setPort(BrokerTestUtils.getPort());
 		connectionFactoryWithReturnsEnabled.setPublisherReturns(true);
+		connectionFactoryWithReturnsEnabled.setApplicationContext(APPLICATION_CONTEXT);
+
 		templateWithReturnsEnabled = new RabbitTemplate(connectionFactoryWithReturnsEnabled);
 		templateWithReturnsEnabled.setMandatory(true);
+
 		connectionFactoryWithConfirmsAndReturnsEnabled = new CachingConnectionFactory();
 		connectionFactoryWithConfirmsAndReturnsEnabled.setHost("localhost");
 		connectionFactoryWithConfirmsAndReturnsEnabled.setChannelCacheSize(100);
 		connectionFactoryWithConfirmsAndReturnsEnabled.setPort(BrokerTestUtils.getPort());
 		connectionFactoryWithConfirmsAndReturnsEnabled.setPublisherConfirmType(ConfirmType.CORRELATED);
 		connectionFactoryWithConfirmsAndReturnsEnabled.setPublisherReturns(true);
+		connectionFactoryWithConfirmsAndReturnsEnabled.setApplicationContext(APPLICATION_CONTEXT);
+
 		templateWithConfirmsAndReturnsEnabled = new RabbitTemplate(connectionFactoryWithConfirmsAndReturnsEnabled);
 		templateWithConfirmsAndReturnsEnabled.setMandatory(true);
 	}
@@ -149,9 +163,19 @@ public void create() {
 	public void cleanUp() {
 		this.templateWithConfirmsEnabled.stop();
 		this.templateWithReturnsEnabled.stop();
+
+		this.connectionFactory.onApplicationEvent(new ContextClosedEvent(APPLICATION_CONTEXT));
 		this.connectionFactory.destroy();
+
+		this.connectionFactoryWithConfirmsEnabled.onApplicationEvent(new ContextClosedEvent(APPLICATION_CONTEXT));
 		this.connectionFactoryWithConfirmsEnabled.destroy();
+
+		this.connectionFactoryWithReturnsEnabled.onApplicationEvent(new ContextClosedEvent(APPLICATION_CONTEXT));
 		this.connectionFactoryWithReturnsEnabled.destroy();
+
+		this.connectionFactoryWithConfirmsAndReturnsEnabled.onApplicationEvent(new ContextClosedEvent(APPLICATION_CONTEXT));
+		this.connectionFactoryWithConfirmsAndReturnsEnabled.destroy();
+
 		this.executorService.shutdown();
 	}
 
@@ -159,7 +183,7 @@ public void cleanUp() {
 	public void testPublisherConfirmReceived() throws Exception {
 		final CountDownLatch latch = new CountDownLatch(10000);
 		final AtomicInteger acks = new AtomicInteger();
-		final AtomicReference confirmCorrelation = new AtomicReference();
+		final AtomicReference confirmCorrelation = new AtomicReference<>();
 		AtomicReference callbackThreadName = new AtomicReference<>();
 		this.templateWithConfirmsEnabled.setConfirmCallback((correlationData, ack, cause) -> {
 			acks.incrementAndGet();
@@ -208,7 +232,7 @@ public Message postProcessMessage(Message message, Correlation correlation, Stri
 		this.templateWithConfirmsEnabled.execute(channel -> {
 			Map listenerMap = TestUtils.getPropertyValue(((ChannelProxy) channel).getTargetChannel(),
 					"listenerForSeq", Map.class);
-			await().until(() -> listenerMap.size() == 0);
+			await().until(listenerMap::isEmpty);
 			return null;
 		});
 
@@ -227,7 +251,8 @@ public void testPublisherConfirmWithSendAndReceive() throws Exception {
 			confirmCD.set(correlationData);
 			latch.countDown();
 		});
-		SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(this.connectionFactoryWithConfirmsEnabled);
+		SimpleMessageListenerContainer container =
+				new SimpleMessageListenerContainer(this.connectionFactoryWithConfirmsEnabled);
 		container.setQueueNames(ROUTE);
 		container.setReceiveTimeout(10);
 		container.setMessageListener(
@@ -643,7 +668,6 @@ public void testConcurrentConfirms() throws Exception {
 		assertThat(waitForAll3AcksLatch.await(10, TimeUnit.SECONDS)).isTrue();
 		assertThat(acks.get()).isEqualTo(3);
 
-
 		channel.basicConsume("foo", false, (Map) null, null);
 		verify(mockChannel).basicConsume("foo", false, (Map) null, null);
 

From 4bd3b1dba9d4da9941843bcff8da2aec6773c188 Mon Sep 17 00:00:00 2001
From: Artem Bilan 
Date: Tue, 14 Jan 2025 15:40:19 -0500
Subject: [PATCH 655/737] Migrate to DCO from CLA

---
 .github/dco.yml   | 2 ++
 CONTRIBUTING.adoc | 8 +++-----
 2 files changed, 5 insertions(+), 5 deletions(-)
 create mode 100644 .github/dco.yml

diff --git a/.github/dco.yml b/.github/dco.yml
new file mode 100644
index 0000000000..0c4b142e9a
--- /dev/null
+++ b/.github/dco.yml
@@ -0,0 +1,2 @@
+require:
+  members: false
diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc
index 849b95168b..042dce5d43 100644
--- a/CONTRIBUTING.adoc
+++ b/CONTRIBUTING.adoc
@@ -23,12 +23,10 @@ Search the https://github.com/spring-projects/spring-integration/issues[GitHub i
 If not, please create a new issue in GitHub before submitting a pull request unless the change is truly trivial, e.g. typo fixes,
 removing compiler warnings, etc.
 
-== Sign the contributor license agreement
+== Developer Certificate of Origin
 
-If you have not previously done so, please fill out and submit the https://cla.pivotal.io/sign/spring[Contributor License Agreement (CLA)].
-
-Very important, before we can accept any *Spring AMQP contributions*, we will need you to sign the CLA.
-Signing the CLA does not grant anyone commit rights to the main repository, but it does mean that we can accept your contributions, and you will get an author credit if we do.
+All commits must include a __Signed-off-by__ trailer at the end of each commit message to indicate that the contributor agrees to the Developer Certificate of Origin.
+For additional details, please refer to the blog post https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring[Hello DCO, Goodbye CLA: Simplifying Contributions to Spring].
 
 == Fork the Repository
 

From c434d873d3c79ad746fc14045f80e4126a7b8c6a Mon Sep 17 00:00:00 2001
From: Artem Bilan 
Date: Thu, 16 Jan 2025 17:30:47 -0500
Subject: [PATCH 656/737] Upgrade to Gradle `8.12`

---
 gradle/wrapper/gradle-wrapper.jar        | Bin 43504 -> 43583 bytes
 gradle/wrapper/gradle-wrapper.properties |   4 ++--
 gradlew                                  |   3 +--
 3 files changed, 3 insertions(+), 4 deletions(-)

diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 2c3521197d7c4586c843d1d3e9090525f1898cde..a4b76b9530d66f5e68d973ea569d8e19de379189 100644
GIT binary patch
delta 3990
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

diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 68e8816d71..e1b837a19c 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,7 +1,7 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
+distributionSha256Sum=7a00d51fb93147819aab76024feece20b6b84e420694101f276be952e08bef03
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
 networkTimeout=10000
 validateDistributionUrl=true
 zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
index f5feea6d6b..f3b75f3b0d 100755
--- a/gradlew
+++ b/gradlew
@@ -86,8 +86,7 @@ done
 # shellcheck disable=SC2034
 APP_BASE_NAME=${0##*/}
 # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
-APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
-' "$PWD" ) || exit
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
 
 # Use the maximum available, or set MAX_FD != -1 to use that value.
 MAX_FD=maximum

From 415a61065dc676d0a7124ec66819cba5a7101f12 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 18 Jan 2025 02:22:31 +0000
Subject: [PATCH 657/737] Bump io.micrometer:micrometer-bom from 1.14.2 to
 1.14.3 (#2943)

Bumps [io.micrometer:micrometer-bom](https://github.com/micrometer-metrics/micrometer) from 1.14.2 to 1.14.3.
- [Release notes](https://github.com/micrometer-metrics/micrometer/releases)
- [Commits](https://github.com/micrometer-metrics/micrometer/compare/v1.14.2...v1.14.3)

---
updated-dependencies:
- dependency-name: io.micrometer:micrometer-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index 280e070d79..4dd9f6043e 100644
--- a/build.gradle
+++ b/build.gradle
@@ -60,7 +60,7 @@ ext {
 	log4jVersion = '2.24.3'
 	logbackVersion = '1.5.16'
 	micrometerDocsVersion = '1.0.4'
-	micrometerVersion = '1.14.2'
+	micrometerVersion = '1.14.3'
 	micrometerTracingVersion = '1.4.1'
 	mockitoVersion = '5.14.2'
 	rabbitmqStreamVersion = '0.18.0'

From 4e73f097c7d69d96e045ca98f43a99aa1ddc8080 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 18 Jan 2025 02:23:50 +0000
Subject: [PATCH 658/737] Bump org.springframework.data:spring-data-bom from
 2024.1.1 to 2024.1.2 (#2942)

Bumps [org.springframework.data:spring-data-bom](https://github.com/spring-projects/spring-data-bom) from 2024.1.1 to 2024.1.2.
- [Release notes](https://github.com/spring-projects/spring-data-bom/releases)
- [Commits](https://github.com/spring-projects/spring-data-bom/compare/2024.1.1...2024.1.2)

---
updated-dependencies:
- dependency-name: org.springframework.data:spring-data-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index 4dd9f6043e..6a12398331 100644
--- a/build.gradle
+++ b/build.gradle
@@ -66,7 +66,7 @@ ext {
 	rabbitmqStreamVersion = '0.18.0'
 	rabbitmqVersion = '5.22.0'
 	reactorVersion = '2024.0.1'
-	springDataVersion = '2024.1.1'
+	springDataVersion = '2024.1.2'
 	springRetryVersion = '2.0.11'
 	springVersion = '6.2.1'
 	testcontainersVersion = '1.20.4'

From 6e1d0a54eb0862e4f8dd0bb130be262339ab7517 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 18 Jan 2025 02:24:32 +0000
Subject: [PATCH 659/737] Bump org.springframework:spring-framework-bom from
 6.2.1 to 6.2.2 (#2944)

Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.2.1 to 6.2.2.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.1...v6.2.2)

---
updated-dependencies:
- dependency-name: org.springframework:spring-framework-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index 6a12398331..8682cfddd7 100644
--- a/build.gradle
+++ b/build.gradle
@@ -68,7 +68,7 @@ ext {
 	reactorVersion = '2024.0.1'
 	springDataVersion = '2024.1.2'
 	springRetryVersion = '2.0.11'
-	springVersion = '6.2.1'
+	springVersion = '6.2.2'
 	testcontainersVersion = '1.20.4'
 
 	javaProjects = subprojects - project(':spring-amqp-bom')

From 7279330ce837ae07eef7016c9fe45c159d30dbc0 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 18 Jan 2025 03:07:56 +0000
Subject: [PATCH 660/737] Bump io.projectreactor:reactor-bom from 2024.0.1 to
 2024.0.2 (#2945)

Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2024.0.1 to 2024.0.2.
- [Release notes](https://github.com/reactor/reactor/releases)
- [Commits](https://github.com/reactor/reactor/compare/2024.0.1...2024.0.2)

---
updated-dependencies:
- dependency-name: io.projectreactor:reactor-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index 8682cfddd7..a501144440 100644
--- a/build.gradle
+++ b/build.gradle
@@ -65,7 +65,7 @@ ext {
 	mockitoVersion = '5.14.2'
 	rabbitmqStreamVersion = '0.18.0'
 	rabbitmqVersion = '5.22.0'
-	reactorVersion = '2024.0.1'
+	reactorVersion = '2024.0.2'
 	springDataVersion = '2024.1.2'
 	springRetryVersion = '2.0.11'
 	springVersion = '6.2.2'

From e9b535be2dce3e334a09e45247a2f0cd9f444eec Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 21 Jan 2025 16:07:59 +0000
Subject: [PATCH 661/737] Bump io.micrometer:micrometer-tracing-bom from 1.4.1
 to 1.4.2 (#2946)

Bumps [io.micrometer:micrometer-tracing-bom](https://github.com/micrometer-metrics/tracing) from 1.4.1 to 1.4.2.
- [Release notes](https://github.com/micrometer-metrics/tracing/releases)
- [Commits](https://github.com/micrometer-metrics/tracing/compare/v1.4.1...v1.4.2)

---
updated-dependencies:
- dependency-name: io.micrometer:micrometer-tracing-bom
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index a501144440..88ecbeefe4 100644
--- a/build.gradle
+++ b/build.gradle
@@ -61,7 +61,7 @@ ext {
 	logbackVersion = '1.5.16'
 	micrometerDocsVersion = '1.0.4'
 	micrometerVersion = '1.14.3'
-	micrometerTracingVersion = '1.4.1'
+	micrometerTracingVersion = '1.4.2'
 	mockitoVersion = '5.14.2'
 	rabbitmqStreamVersion = '0.18.0'
 	rabbitmqVersion = '5.22.0'

From cfdc239c8795059daf138f326fb2e0b471ae3248 Mon Sep 17 00:00:00 2001
From: Spring Builds 
Date: Tue, 21 Jan 2025 18:03:30 +0000
Subject: [PATCH 662/737] [artifactory-release] Release version 3.2.2

---
 gradle.properties | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gradle.properties b/gradle.properties
index 8e33f5cdb5..109b8329a7 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
-version=3.2.2-SNAPSHOT
+version=3.2.2
 org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8
 org.gradle.daemon=true
 org.gradle.caching=true

From 140f23abfe71bf29f010d7fc367f153308d2899a Mon Sep 17 00:00:00 2001
From: Spring Builds 
Date: Tue, 21 Jan 2025 18:03:31 +0000
Subject: [PATCH 663/737] [artifactory-release] Next development version

---
 gradle.properties | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gradle.properties b/gradle.properties
index 109b8329a7..6d61f0a56d 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
-version=3.2.2
+version=3.2.3-SNAPSHOT
 org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8
 org.gradle.daemon=true
 org.gradle.caching=true

From d1ce0dc7c9bf4a5dd1059db4bbdc0da68d8e9268 Mon Sep 17 00:00:00 2001
From: Artem Bilan 
Date: Wed, 22 Jan 2025 12:49:28 -0500
Subject: [PATCH 664/737] Fix `Implementation-Vendor` for manifest file to
 Broadcom

---
 build.gradle | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/build.gradle b/build.gradle
index 88ecbeefe4..35c9d41acf 100644
--- a/build.gradle
+++ b/build.gradle
@@ -321,7 +321,7 @@ configure(javaProjects) { subproject ->
 					'Created-By': "JDK ${System.properties['java.version']} (${System.properties['java.specification.vendor']})",
 					'Implementation-Title': subproject.name,
 					'Implementation-Vendor-Id': subproject.group,
-					'Implementation-Vendor': 'VMware Inc.',
+					'Implementation-Vendor': 'Broadcom Inc.',
 					'Implementation-URL': linkHomepage,
 					'Automatic-Module-Name': subproject.name.replace('-', '.')  // for Jigsaw
 			)

From fdc7e4d8edfb456f2a5ff30e1dd6b3e35f613174 Mon Sep 17 00:00:00 2001
From: Artem Bilan 
Date: Mon, 27 Jan 2025 10:10:51 -0500
Subject: [PATCH 665/737] Fix race condition in the
 `RabbitTemplatePublisherCallbacksIntegration3Tests`

---
 ...latePublisherCallbacksIntegration3Tests.java | 17 ++++++++++++-----
 1 file changed, 12 insertions(+), 5 deletions(-)

diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration3Tests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration3Tests.java
index 1c489a7dbc..a04f4338cf 100644
--- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration3Tests.java
+++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration3Tests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-2024 the original author or authors.
+ * Copyright 2018-2025 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.
@@ -46,9 +46,9 @@
  * @since 2.1
  *
  */
-@RabbitAvailable(queues = { RabbitTemplatePublisherCallbacksIntegration3Tests.QUEUE1,
+@RabbitAvailable(queues = {RabbitTemplatePublisherCallbacksIntegration3Tests.QUEUE1,
 		RabbitTemplatePublisherCallbacksIntegration3Tests.QUEUE2,
-		RabbitTemplatePublisherCallbacksIntegration3Tests.QUEUE3 })
+		RabbitTemplatePublisherCallbacksIntegration3Tests.QUEUE3})
 public class RabbitTemplatePublisherCallbacksIntegration3Tests {
 
 	public static final String QUEUE1 = "synthetic.nack";
@@ -117,9 +117,11 @@ public void testDeferredChannelCacheNack() throws Exception {
 
 	@Test
 	public void testDeferredChannelCacheAck() throws Exception {
-		final CachingConnectionFactory cf = new CachingConnectionFactory(
-				RabbitAvailableCondition.getBrokerRunning().getConnectionFactory());
+		CachingConnectionFactory cf =
+				new CachingConnectionFactory(RabbitAvailableCondition.getBrokerRunning().getConnectionFactory());
 		cf.setPublisherConfirmType(ConfirmType.CORRELATED);
+		ApplicationContext mockApplicationContext = mock();
+		cf.setApplicationContext(mockApplicationContext);
 		final RabbitTemplate template = new RabbitTemplate(cf);
 		final CountDownLatch confirmLatch = new CountDownLatch(1);
 		final AtomicInteger cacheCount = new AtomicInteger();
@@ -138,6 +140,7 @@ public void testDeferredChannelCacheAck() throws Exception {
 		template.convertAndSend("", QUEUE2, "foo", new CorrelationData("foo"));
 		assertThat(confirmLatch.await(10, TimeUnit.SECONDS)).isTrue();
 		assertThat(cacheCount.get()).isEqualTo(1);
+		cf.onApplicationEvent(new ContextClosedEvent(mockApplicationContext));
 		cf.destroy();
 	}
 
@@ -146,6 +149,8 @@ public void testTwoSendsAndReceivesDRTMLC() throws Exception {
 		CachingConnectionFactory cf = new CachingConnectionFactory(
 				RabbitAvailableCondition.getBrokerRunning().getConnectionFactory());
 		cf.setPublisherConfirmType(ConfirmType.CORRELATED);
+		ApplicationContext mockApplicationContext = mock();
+		cf.setApplicationContext(mockApplicationContext);
 		RabbitTemplate template = new RabbitTemplate(cf);
 		template.setReplyTimeout(0);
 		final CountDownLatch confirmLatch = new CountDownLatch(2);
@@ -157,6 +162,8 @@ public void testTwoSendsAndReceivesDRTMLC() throws Exception {
 		assertThat(confirmLatch.await(10, TimeUnit.SECONDS)).isTrue();
 		assertThat(template.receive(QUEUE3, 10_000)).isNotNull();
 		assertThat(template.receive(QUEUE3, 10_000)).isNotNull();
+		cf.onApplicationEvent(new ContextClosedEvent(mockApplicationContext));
+		cf.destroy();
 	}
 
 }

From 2aaa8db7d0b6a16a9c01f3c08e652f6bef2cb6ee Mon Sep 17 00:00:00 2001
From: Artem Bilan 
Date: Tue, 28 Jan 2025 15:44:03 -0500
Subject: [PATCH 666/737] Migrate imports order to other Spring projects style

---
 eclipse-code-formatter.xml                    | 313 ------------
 .../AbstractJackson2MessageConverter.java     |   7 +-
 .../converter/AbstractJavaTypeMapper.java     |   8 +-
 .../DefaultJackson2JavaTypeMapper.java        |   8 +-
 .../converter/Jackson2JavaTypeMapper.java     |   6 +-
 .../Jackson2JsonMessageConverter.java         |   8 +-
 .../Jackson2XmlMessageConverter.java          |   8 +-
 .../amqp/support/converter/JacksonUtils.java  |   6 +-
 .../converter/ProjectingMessageConverter.java |   8 +-
 .../amqp/core/AddressTests.java               |   6 +-
 .../amqp/core/BindingBuilderTests.java        |   6 +-
 .../BindingBuilderWithLazyQueueNameTests.java |   6 +-
 .../amqp/core/DeclarablesTests.java           |   6 +-
 .../amqp/core/MessagePropertiesTests.java     |   6 +-
 .../amqp/core/MessageTests.java               |   6 +-
 .../amqp/core/QueueBuilderTests.java          |   6 +-
 .../amqp/core/QueueNameTests.java             |   6 +-
 .../amqp/core/builder/BuilderTests.java       |   8 +-
 .../core/builder/MessageBuilderTests.java     |   6 +-
 .../AmqpMessageHeaderAccessorTests.java       |   8 +-
 .../MessagePostProcessorUtilsTests.java       |   6 +-
 .../support/SimpleAmqpHeaderMapperTests.java  |  10 +-
 ...istDeserializingMessageConverterTests.java |   8 +-
 ...ntTypeDelegatingMessageConverterTests.java |   8 +-
 .../converter/DefaultClassMapperTests.java    |   8 +-
 .../DefaultJackson2JavaTypeMapperTests.java   |  17 +-
 .../Jackson2JsonMessageConverterTests.java    |  21 +-
 .../Jackson2XmlMessageConverterTests.java     |   9 +-
 .../MarshallingMessageConverterTests.java     |   6 +-
 .../MessagingMessageConverterTests.java       |   8 +-
 .../SerializerMessageConverterTests.java      |   8 +-
 .../SimpleMessageConverterTests.java          |  10 +-
 .../amqp/rabbit/junit/BrokerRunning.java      |   9 +-
 .../rabbit/junit/BrokerRunningSupport.java    |   9 +-
 .../junit/RabbitAvailableCondition.java       |   5 +-
 .../amqp/rabbit/junit/BrokerRunningTests.java |   7 +-
 .../RabbitAvailableCTORInjectionTests.java    |   9 +-
 .../rabbit/junit/RabbitAvailableTests.java    |   9 +-
 .../StreamRabbitListenerContainerFactory.java |   5 +-
 .../listener/StreamListenerContainer.java     |  15 +-
 .../listener/StreamMessageListener.java       |   6 +-
 .../adapter/StreamMessageListenerAdapter.java |   8 +-
 .../RabbitStreamMessageReceiverContext.java   |  10 +-
 .../RabbitStreamTemplateObservation.java      |   6 +-
 .../producer/RabbitStreamOperations.java      |   6 +-
 .../stream/producer/RabbitStreamTemplate.java |  20 +-
 .../stream/retry/StreamMessageRecoverer.java  |   6 +-
 ...RetryOperationsInterceptorFactoryBean.java |   8 +-
 .../rabbit/stream/support/StreamAdmin.java    |   8 +-
 .../support/StreamMessageProperties.java      |   6 +-
 .../DefaultStreamMessageConverter.java        |  16 +-
 .../config/SuperStreamProvisioningTests.java  |   6 +-
 .../stream/listener/RabbitListenerTests.java  |  25 +-
 .../StreamListenerContainerTests.java         |  25 +-
 .../SuperStreamConcurrentSACTests.java        |   9 +-
 .../stream/listener/SuperStreamSACTests.java  |   9 +-
 .../stream/micrometer/TracingTests.java       |  23 +-
 .../producer/RabbitStreamTemplateTests.java   |  29 +-
 .../DefaultStreamMessageConverterTests.java   |  11 +-
 .../amqp/rabbit/test/TestRabbitTemplate.java  |  20 +-
 .../AbstractRabbitAnnotationDrivenTests.java  |   6 +-
 .../rabbit/test/RabbitListenerProxyTest.java  |  12 +-
 .../test/context/SpringRabbitTestTests.java   |   6 +-
 .../ExampleRabbitListenerCaptureTest.java     |   6 +-
 ...xampleRabbitListenerSpyAndCaptureTest.java |  12 +-
 .../ExampleRabbitListenerSpyTest.java         |  12 +-
 .../examples/TestRabbitTemplateTests.java     |  21 +-
 .../amqp/rabbit/test/mockito/AnswerTests.java |  10 +-
 .../amqp/rabbit/AsyncRabbitTemplate.java      |   5 +-
 .../connection/AbstractConnectionFactory.java |  21 +-
 .../connection/CachingConnectionFactory.java  |  18 +-
 .../amqp/rabbit/connection/ChannelProxy.java  |   6 +-
 .../connection/ClosingRecoveryListener.java   |   7 +-
 .../amqp/rabbit/connection/Connection.java    |   8 +-
 .../connection/ConnectionFactoryUtils.java    |   6 +-
 .../connection/ConsumerChannelRegistry.java   |   5 +-
 .../PooledChannelConnectionFactory.java       |   9 +-
 .../PublisherCallbackChannelImpl.java         |  27 +-
 .../rabbit/connection/RabbitAccessor.java     |   7 +-
 .../RabbitConnectionFactoryBean.java          |  20 +-
 .../connection/RabbitResourceHolder.java      |   5 +-
 .../amqp/rabbit/connection/RabbitUtils.java   |  17 +-
 .../rabbit/connection/SimpleConnection.java   |  14 +-
 .../ThreadChannelConnectionFactory.java       |   9 +-
 .../amqp/rabbit/core/ChannelCallback.java     |   6 +-
 .../amqp/rabbit/core/RabbitAdmin.java         |   9 +-
 .../amqp/rabbit/core/RabbitTemplate.java      |  30 +-
 .../AbstractMessageListenerContainer.java     |   9 +-
 .../listener/BlockingQueueConsumer.java       |  15 +-
 .../DirectMessageListenerContainer.java       |  13 +-
 ...DirectReplyToMessageListenerContainer.java |   6 +-
 .../rabbit/listener/MicrometerHolder.java     |  10 +-
 .../SimpleMessageListenerContainer.java       |  10 +-
 .../AbstractAdaptableMessageListener.java     |   5 +-
 .../BatchMessagingMessageListenerAdapter.java |   6 +-
 ...inuationHandlerMethodArgumentResolver.java |   6 +-
 .../adapter/MessageListenerAdapter.java       |   6 +-
 .../MessagingMessageListenerAdapter.java      |   6 +-
 .../api/ChannelAwareBatchMessageListener.java |   6 +-
 .../api/ChannelAwareMessageListener.java      |   6 +-
 .../api/RabbitListenerErrorHandler.java       |   6 +-
 .../amqp/rabbit/log4j2/AmqpAppender.java      |   5 +-
 .../amqp/rabbit/logback/AmqpAppender.java     |  22 +-
 .../DefaultMessagePropertiesConverter.java    |  10 +-
 .../support/MessagePropertiesConverter.java   |   8 +-
 .../support/RabbitExceptionTranslator.java    |  10 +-
 .../RabbitMessageReceiverContext.java         |   6 +-
 .../RabbitMessageSenderContext.java           |   6 +-
 .../amqp/rabbit/config/spring-rabbit.xsd      |   7 +-
 .../amqp/rabbit/AsyncRabbitTemplateTests.java |  20 +-
 .../AbstractRabbitAnnotationDrivenTests.java  |  18 +-
 .../AnnotationDrivenNamespaceTests.java       |   7 +-
 .../rabbit/annotation/AsyncListenerTests.java |   7 +-
 .../ComplexTypeJsonIntegrationTests.java      |   8 +-
 .../annotation/ConsumerBatchingTests.java     |  15 +-
 ...atingMessageConverterIntegrationTests.java |   6 +-
 .../EnableRabbitBatchIntegrationTests.java    |   6 +-
 ...EnableRabbitBatchJsonIntegrationTests.java |   6 +-
 .../EnableRabbitCglibProxyTests.java          |   8 +-
 .../EnableRabbitIdleContainerTests.java       |   6 +-
 .../EnableRabbitIntegrationTests.java         |  31 +-
 .../EnableRabbitReturnTypesTests.java         |   6 +-
 .../rabbit/annotation/EnableRabbitTests.java  |  20 +-
 .../rabbit/annotation/LazyContainerTests.java |   6 +-
 .../annotation/MessageHandlerTests.java       |   9 +-
 .../annotation/MockMultiRabbitTests.java      |   5 +-
 ...ltiRabbitBootstrapConfigurationTests.java} |   9 +-
 .../annotation/OptionalPayloadTests.java      |   9 +-
 ...tenerAnnotationBeanPostProcessorTests.java |   6 +-
 .../amqp/rabbit/config/AdminParserTests.java  |   8 +-
 .../CompositeContainerCustomizerTests.java    |   8 +-
 .../config/ConnectionFactoryParserTests.java  |   9 +-
 .../ExchangeParserIntegrationTests.java       |   6 +-
 .../rabbit/config/ExchangeParserTests.java    |   6 +-
 .../ListenerContainerFactoryBeanTests.java    |   8 +-
 .../config/ListenerContainerParserTests.java  |   8 +-
 ...stenerContainerPlaceholderParserTests.java |   6 +-
 .../MismatchedQueueDeclarationTests.java      |   9 +-
 .../config/QueueArgumentsParserTests.java     |   6 +-
 .../config/QueueParserIntegrationTests.java   |   8 +-
 .../amqp/rabbit/config/QueueParserTests.java  |   8 +-
 ...tenerContainerFactoryIntegrationTests.java |   9 +-
 .../RabbitListenerContainerFactoryTests.java  |   9 +-
 .../RabbitListenerContainerTestFactory.java   |   6 +-
 .../config/RabbitNamespaceHandlerTests.java   |   6 +-
 .../RetryInterceptorBuilderSupportTests.java  |  10 +-
 .../SimpleRabbitListenerEndpointTests.java    |  11 +-
 .../rabbit/config/TemplateParserTests.java    |   6 +-
 .../AbstractConnectionFactoryTests.java       |  37 +-
 .../connection/CachePropertiesTests.java      |   7 +-
 ...hingConnectionFactoryIntegrationTests.java |  61 ++-
 .../CachingConnectionFactoryTests.java        |  63 ++-
 .../ClientRecoveryCompatibilityTests.java     |  15 +-
 .../ConnectionFactoryContextWrapperTests.java |  10 +-
 .../ConnectionFactoryLifecycleTests.java      |  15 +-
 .../ConnectionFactoryUtilsTests.java          |   8 +-
 .../connection/ConnectionListenerTests.java   |   8 +-
 .../ConsumerConnectionRecoveryTests.java      |   6 +-
 ...ueueConnectionFactoryIntegrationTests.java |  19 +-
 .../LocalizedQueueConnectionFactoryTests.java |  46 +-
 .../rabbit/connection/NodeLocatorTests.java   |  12 +-
 .../PooledChannelConnectionFactoryTests.java  |   9 +-
 .../PublisherCallbackChannelTests.java        |  29 +-
 .../RabbitReconnectProblemTests.java          |  20 +-
 .../RoutingConnectionFactoryTests.java        |  27 +-
 .../rabbit/connection/SSLConnectionTests.java |  27 +-
 .../connection/SingleConnectionFactory.java   |   8 +-
 .../SingleConnectionFactoryTests.java         |  19 +-
 .../ThreadChannelConnectionFactoryTests.java  |  19 +-
 .../core/BatchingRabbitTemplateTests.java     |  23 +-
 .../core/FixedReplyQueueDeadLetterTests.java  |   8 +-
 .../core/MessagingTemplateConfirmsTests.java  |   6 +-
 .../core/RabbitAdminDeclarationTests.java     |  35 +-
 .../core/RabbitAdminIntegrationTests.java     |  19 +-
 .../amqp/rabbit/core/RabbitAdminTests.java    |  45 +-
 .../core/RabbitBindingIntegrationTests.java   |   8 +-
 .../core/RabbitGatewaySupportTests.java       |   8 +-
 .../core/RabbitMessagingTemplateTests.java    |  24 +-
 ...irectReplyToContainerIntegrationTests.java |  11 +-
 .../core/RabbitTemplateHeaderTests.java       |  23 +-
 .../core/RabbitTemplateIntegrationTests.java  |  53 +-
 .../RabbitTemplateMPPIntegrationTests.java    |   6 +-
 ...itTemplatePerformanceIntegrationTests.java |   6 +-
 ...tePublisherCallbacksIntegration1Tests.java |  33 +-
 ...tePublisherCallbacksIntegration2Tests.java |  11 +-
 ...tePublisherCallbacksIntegration3Tests.java |   7 +-
 ...tingConnectionFactoryIntegrationTests.java |   6 +-
 .../amqp/rabbit/core/RabbitTemplateTests.java |  63 ++-
 .../core/SimplePublisherConfirmsTests.java    |   6 +-
 .../core/TransactionalEventListenerTests.java |  13 +-
 .../rabbit/listener/AsyncReplyToTests.java    |   7 +-
 ...BlockingQueueConsumerIntegrationTests.java |   8 +-
 .../listener/BlockingQueueConsumerTests.java  |  43 +-
 .../BrokerDeclaredQueueNameTests.java         |   6 +-
 .../listener/BrokerEventListenerTests.java    |   6 +-
 .../rabbit/listener/ContainerAdminTests.java  |   6 +-
 .../ContainerInitializationTests.java         |  10 +-
 .../listener/ContainerShutDownTests.java      |   7 +-
 .../rabbit/listener/ContainerUtilsTests.java  |   8 +-
 ...sageListenerContainerIntegrationTests.java |  29 +-
 ...rectMessageListenerContainerMockTests.java |  43 +-
 ...tReplyToMessageListenerContainerTests.java |  16 +-
 .../amqp/rabbit/listener/DlqExpiryTests.java  |   6 +-
 .../rabbit/listener/ErrorHandlerTests.java    |  16 +-
 .../listener/ExternalTxManagerSMLCTests.java  |  37 +-
 .../listener/ExternalTxManagerTests.java      |  39 +-
 .../JavaConfigFixedReplyQueueTests.java       |   8 +-
 .../ListenFromAutoDeleteQueueTests.java       |  24 +-
 .../listener/LocallyTransactedTests.java      |  39 +-
 ...ContainerErrorHandlerIntegrationTests.java |  27 +-
 ...nerContainerLifecycleIntegrationTests.java |  17 +-
 ...ontainerMultipleQueueIntegrationTests.java |   6 +-
 ...istenerContainerRetryIntegrationTests.java |   8 +-
 .../MessageListenerContainerTxSynchTests.java |  33 +-
 ...sageListenerManualAckIntegrationTests.java |   9 +-
 ...veryCachingConnectionIntegrationTests.java |  11 +-
 ...istenerRecoveryRepeatIntegrationTests.java |   7 +-
 ...MessageListenerTxSizeIntegrationTests.java |  11 +-
 .../MethodRabbitListenerEndpointTests.java    |  23 +-
 .../listener/MicrometerHolderTests.java       |  15 +-
 .../listener/QueueDeclarationTests.java       |  31 +-
 .../RabbitListenerEndpointRegistrarTests.java |  10 +-
 .../RabbitListenerEndpointRegistryTests.java  |   8 +-
 ...ageListenerContainerIntegration2Tests.java |  27 +-
 ...sageListenerContainerIntegrationTests.java |  11 +-
 ...mpleMessageListenerContainerLongTests.java |  17 +-
 .../SimpleMessageListenerContainerTests.java  |  57 ++-
 .../SimpleMessageListenerWithRabbitMQ.java    |   6 +-
 .../listener/StopStartIntegrationTests.java   |   6 +-
 .../listener/UnackedRawIntegrationTests.java  |  19 +-
 ...hMessagingMessageListenerAdapterTests.java |   9 +-
 .../DelegatingInvocableHandlerTests.java      |   8 +-
 .../adapter/MessageListenerAdapterTests.java  |  21 +-
 .../MessagingMessageListenerAdapterTests.java |  21 +-
 .../amqp/rabbit/log4j2/AmqpAppenderTests.java |  41 +-
 .../log4j2/ExtendAmqpAppenderTests.java       |  14 +-
 .../logback/AmqpAppenderIntegrationTests.java |  15 +-
 .../rabbit/logback/AmqpAppenderTests.java     |  31 +-
 .../rabbit/retry/MissingIdRetryTests.java     |  22 +-
 ...blishMessageRecovererIntegrationTests.java |   6 +-
 .../retry/RepublishMessageRecovererTests.java |   8 +-
 ...RecovererWithConfirmsIntegrationTests.java |  10 +-
 .../support/ActiveObjectCounterTests.java     |   6 +-
 ...efaultMessagePropertiesConverterTests.java |  13 +-
 .../ObservationIntegrationTests.java          |  24 +-
 .../support/micrometer/ObservationTests.java  |  41 +-
 .../RabbitExceptionTranslatorTests.java       |  10 +-
 ...bitTransactionManagerIntegrationTests.java |  10 +-
 src/checkstyle/checkstyle.xml                 |   4 +-
 src/eclipse/org.eclipse.core.resources.prefs  |   2 +
 src/eclipse/org.eclipse.jdt.core.prefs        | 469 ++++++++++++++++++
 src/eclipse/org.eclipse.jdt.ui.prefs          |  66 +++
 src/idea/spring-framework.xml                 | 269 ++++++++++
 253 files changed, 2428 insertions(+), 2027 deletions(-)
 delete mode 100644 eclipse-code-formatter.xml
 rename spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/{MultiRabbitBootstrapConfigurationTest.java => MultiRabbitBootstrapConfigurationTests.java} (96%)
 create mode 100644 src/eclipse/org.eclipse.core.resources.prefs
 create mode 100644 src/eclipse/org.eclipse.jdt.core.prefs
 create mode 100644 src/eclipse/org.eclipse.jdt.ui.prefs
 create mode 100644 src/idea/spring-framework.xml

diff --git a/eclipse-code-formatter.xml b/eclipse-code-formatter.xml
deleted file mode 100644
index e2a217491d..0000000000
--- a/eclipse-code-formatter.xml
+++ /dev/null
@@ -1,313 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java
index e9cb03ab6d..01c76a4c08 100644
--- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java
+++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-2024 the original author or authors.
+ * Copyright 2018-2025 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.
@@ -23,6 +23,8 @@
 import java.nio.charset.StandardCharsets;
 import java.util.Optional;
 
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
@@ -36,9 +38,6 @@
 import org.springframework.util.MimeType;
 import org.springframework.util.MimeTypeUtils;
 
-import com.fasterxml.jackson.databind.JavaType;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
 /**
  * Abstract Jackson2 message converter.
  *
diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJavaTypeMapper.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJavaTypeMapper.java
index e19c62dc17..5aef390e2c 100644
--- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJavaTypeMapper.java
+++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJavaTypeMapper.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -20,14 +20,14 @@
 import java.util.HashMap;
 import java.util.Map;
 
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.type.TypeFactory;
+
 import org.springframework.amqp.core.MessageProperties;
 import org.springframework.beans.factory.BeanClassLoaderAware;
 import org.springframework.lang.Nullable;
 import org.springframework.util.ClassUtils;
 
-import com.fasterxml.jackson.databind.JavaType;
-import com.fasterxml.jackson.databind.type.TypeFactory;
-
 /**
  * Abstract type mapper.
  *
diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapper.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapper.java
index 380892662b..fa97d5f2fb 100644
--- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapper.java
+++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapper.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -21,14 +21,14 @@
 import java.util.List;
 import java.util.Set;
 
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.type.TypeFactory;
+
 import org.springframework.amqp.core.MessageProperties;
 import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 
-import com.fasterxml.jackson.databind.JavaType;
-import com.fasterxml.jackson.databind.type.TypeFactory;
-
 /**
  * Jackson 2 type mapper.
  * @author Mark Pollack
diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JavaTypeMapper.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JavaTypeMapper.java
index aea13ae7ea..4f0391d8bb 100644
--- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JavaTypeMapper.java
+++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JavaTypeMapper.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -16,11 +16,11 @@
 
 package org.springframework.amqp.support.converter;
 
+import com.fasterxml.jackson.databind.JavaType;
+
 import org.springframework.amqp.core.MessageProperties;
 import org.springframework.lang.Nullable;
 
-import com.fasterxml.jackson.databind.JavaType;
-
 /**
  * Strategy for setting metadata on messages such that one can create the class that needs
  * to be instantiated when receiving a message.
diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverter.java
index 3045ecd013..b69e1b5ade 100644
--- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverter.java
+++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverter.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2023 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -16,12 +16,12 @@
 
 package org.springframework.amqp.support.converter;
 
-import org.springframework.amqp.core.MessageProperties;
-import org.springframework.util.MimeTypeUtils;
-
 import com.fasterxml.jackson.databind.DeserializationFeature;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
+import org.springframework.amqp.core.MessageProperties;
+import org.springframework.util.MimeTypeUtils;
+
 /**
  * JSON converter that uses the Jackson 2 Json library.
  *
diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2XmlMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2XmlMessageConverter.java
index f544fdfa18..27b0c3379d 100644
--- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2XmlMessageConverter.java
+++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2XmlMessageConverter.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-2019 the original author or authors.
+ * Copyright 2018-2025 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.
@@ -16,12 +16,12 @@
 
 package org.springframework.amqp.support.converter;
 
-import org.springframework.amqp.core.MessageProperties;
-import org.springframework.util.MimeTypeUtils;
-
 import com.fasterxml.jackson.databind.DeserializationFeature;
 import com.fasterxml.jackson.dataformat.xml.XmlMapper;
 
+import org.springframework.amqp.core.MessageProperties;
+import org.springframework.util.MimeTypeUtils;
+
 /**
  * XML converter that uses the Jackson 2 Xml library.
  *
diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/JacksonUtils.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/JacksonUtils.java
index f988ed5ba4..f9c0348fff 100644
--- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/JacksonUtils.java
+++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/JacksonUtils.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 the original author or authors.
+ * Copyright 2023-2025 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.
@@ -16,13 +16,13 @@
 
 package org.springframework.amqp.support.converter;
 
-import org.springframework.util.ClassUtils;
-
 import com.fasterxml.jackson.databind.DeserializationFeature;
 import com.fasterxml.jackson.databind.MapperFeature;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.json.JsonMapper;
 
+import org.springframework.util.ClassUtils;
+
 
 /**
  * The utilities for Jackson {@link ObjectMapper} instances.
diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/ProjectingMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/ProjectingMessageConverter.java
index 2b3b7b8d9d..b93901ad2c 100644
--- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/ProjectingMessageConverter.java
+++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/ProjectingMessageConverter.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2019 the original author or authors.
+ * Copyright 2019-2025 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.
@@ -19,6 +19,9 @@
 import java.io.ByteArrayInputStream;
 import java.lang.reflect.Type;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
+
 import org.springframework.amqp.core.Message;
 import org.springframework.core.ResolvableType;
 import org.springframework.data.projection.MethodInterceptorFactory;
@@ -27,9 +30,6 @@
 import org.springframework.data.web.JsonProjectingMethodInterceptorFactory;
 import org.springframework.util.Assert;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
-
 /**
  * Uses a Spring Data {@link ProjectionFactory} to bind incoming messages to projection
  * interfaces.
diff --git a/spring-amqp/src/test/java/org/springframework/amqp/core/AddressTests.java b/spring-amqp/src/test/java/org/springframework/amqp/core/AddressTests.java
index 17c24e4130..3bb17bf155 100644
--- a/spring-amqp/src/test/java/org/springframework/amqp/core/AddressTests.java
+++ b/spring-amqp/src/test/java/org/springframework/amqp/core/AddressTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -16,10 +16,10 @@
 
 package org.springframework.amqp.core;
 
-import static org.assertj.core.api.Assertions.assertThat;
-
 import org.junit.jupiter.api.Test;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 /**
  * @author Mark Pollack
  * @author Mark Fisher
diff --git a/spring-amqp/src/test/java/org/springframework/amqp/core/BindingBuilderTests.java b/spring-amqp/src/test/java/org/springframework/amqp/core/BindingBuilderTests.java
index d8aa174288..e11b3b5adb 100644
--- a/spring-amqp/src/test/java/org/springframework/amqp/core/BindingBuilderTests.java
+++ b/spring-amqp/src/test/java/org/springframework/amqp/core/BindingBuilderTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -16,13 +16,13 @@
 
 package org.springframework.amqp.core;
 
-import static org.assertj.core.api.Assertions.assertThat;
-
 import java.util.Collections;
 
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 /**
  * @author Mark Fisher
  * @author Artem Yakshin
diff --git a/spring-amqp/src/test/java/org/springframework/amqp/core/BindingBuilderWithLazyQueueNameTests.java b/spring-amqp/src/test/java/org/springframework/amqp/core/BindingBuilderWithLazyQueueNameTests.java
index 5fe956b5a8..8c13b96a85 100644
--- a/spring-amqp/src/test/java/org/springframework/amqp/core/BindingBuilderWithLazyQueueNameTests.java
+++ b/spring-amqp/src/test/java/org/springframework/amqp/core/BindingBuilderWithLazyQueueNameTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 the original author or authors.
+ * Copyright 2023-2025 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.
@@ -16,13 +16,13 @@
 
 package org.springframework.amqp.core;
 
-import static org.assertj.core.api.Assertions.assertThat;
-
 import java.util.Collections;
 
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 /**
  * Copy of {@link BindingBuilderTests} but using a queue with a lazy name.
  *
diff --git a/spring-amqp/src/test/java/org/springframework/amqp/core/DeclarablesTests.java b/spring-amqp/src/test/java/org/springframework/amqp/core/DeclarablesTests.java
index a1a9f12f61..832df72ad3 100644
--- a/spring-amqp/src/test/java/org/springframework/amqp/core/DeclarablesTests.java
+++ b/spring-amqp/src/test/java/org/springframework/amqp/core/DeclarablesTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021 the original author or authors.
+ * Copyright 2021-2025 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.
@@ -16,12 +16,12 @@
 
 package org.springframework.amqp.core;
 
-import static org.assertj.core.api.Assertions.assertThat;
-
 import java.util.List;
 
 import org.junit.jupiter.api.Test;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 /**
  * @author Björn Michael
  * @since 2.4
diff --git a/spring-amqp/src/test/java/org/springframework/amqp/core/MessagePropertiesTests.java b/spring-amqp/src/test/java/org/springframework/amqp/core/MessagePropertiesTests.java
index 2d080e8ea4..7899189b69 100644
--- a/spring-amqp/src/test/java/org/springframework/amqp/core/MessagePropertiesTests.java
+++ b/spring-amqp/src/test/java/org/springframework/amqp/core/MessagePropertiesTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -16,13 +16,13 @@
 
 package org.springframework.amqp.core;
 
-import static org.assertj.core.api.Assertions.assertThat;
-
 import java.util.HashSet;
 import java.util.Set;
 
 import org.junit.jupiter.api.Test;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 
 /**
  * @author Dave Syer
diff --git a/spring-amqp/src/test/java/org/springframework/amqp/core/MessageTests.java b/spring-amqp/src/test/java/org/springframework/amqp/core/MessageTests.java
index 4432d7f3e2..83977dcc1a 100644
--- a/spring-amqp/src/test/java/org/springframework/amqp/core/MessageTests.java
+++ b/spring-amqp/src/test/java/org/springframework/amqp/core/MessageTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -16,8 +16,6 @@
 
 package org.springframework.amqp.core;
 
-import static org.assertj.core.api.Assertions.assertThat;
-
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.ObjectInputStream;
@@ -33,6 +31,8 @@
 import org.springframework.amqp.support.converter.SimpleMessageConverter;
 import org.springframework.amqp.utils.SerializationUtils;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 /**
  * @author Mark Fisher
  * @author Dave Syer
diff --git a/spring-amqp/src/test/java/org/springframework/amqp/core/QueueBuilderTests.java b/spring-amqp/src/test/java/org/springframework/amqp/core/QueueBuilderTests.java
index 93edc1b1a1..7522fe2f4a 100644
--- a/spring-amqp/src/test/java/org/springframework/amqp/core/QueueBuilderTests.java
+++ b/spring-amqp/src/test/java/org/springframework/amqp/core/QueueBuilderTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016-2019 the original author or authors.
+ * Copyright 2016-2025 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.
@@ -16,13 +16,13 @@
 
 package org.springframework.amqp.core;
 
-import static org.assertj.core.api.Assertions.assertThat;
-
 import java.util.HashMap;
 import java.util.Map;
 
 import org.junit.jupiter.api.Test;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 /**
  * Tests for {@link QueueBuilder}
  *
diff --git a/spring-amqp/src/test/java/org/springframework/amqp/core/QueueNameTests.java b/spring-amqp/src/test/java/org/springframework/amqp/core/QueueNameTests.java
index e5ac74fa6c..75558a8a14 100644
--- a/spring-amqp/src/test/java/org/springframework/amqp/core/QueueNameTests.java
+++ b/spring-amqp/src/test/java/org/springframework/amqp/core/QueueNameTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015-2019 the original author or authors.
+ * Copyright 2015-2025 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.
@@ -16,12 +16,12 @@
 
 package org.springframework.amqp.core;
 
-import static org.assertj.core.api.Assertions.assertThat;
-
 import java.util.regex.Pattern;
 
 import org.junit.jupiter.api.Test;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 /**
  * @author Gary Russell
  * @since 1.5.3
diff --git a/spring-amqp/src/test/java/org/springframework/amqp/core/builder/BuilderTests.java b/spring-amqp/src/test/java/org/springframework/amqp/core/builder/BuilderTests.java
index 7bf19eec29..da475cc54a 100644
--- a/spring-amqp/src/test/java/org/springframework/amqp/core/builder/BuilderTests.java
+++ b/spring-amqp/src/test/java/org/springframework/amqp/core/builder/BuilderTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016-2024 the original author or authors.
+ * Copyright 2016-2025 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.
@@ -16,9 +16,6 @@
 
 package org.springframework.amqp.core.builder;
 
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
-
 import org.junit.jupiter.api.Test;
 
 import org.springframework.amqp.core.ConsistentHashExchange;
@@ -31,6 +28,9 @@
 import org.springframework.amqp.core.QueueBuilder;
 import org.springframework.amqp.core.TopicExchange;
 
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
 /**
  * @author Gary Russell
  * @author Artem Bilan
diff --git a/spring-amqp/src/test/java/org/springframework/amqp/core/builder/MessageBuilderTests.java b/spring-amqp/src/test/java/org/springframework/amqp/core/builder/MessageBuilderTests.java
index 1c143da779..e59b149690 100644
--- a/spring-amqp/src/test/java/org/springframework/amqp/core/builder/MessageBuilderTests.java
+++ b/spring-amqp/src/test/java/org/springframework/amqp/core/builder/MessageBuilderTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2014-2019 the original author or authors.
+ * Copyright 2014-2025 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.
@@ -16,8 +16,6 @@
 
 package org.springframework.amqp.core.builder;
 
-import static org.assertj.core.api.Assertions.assertThat;
-
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Date;
@@ -31,6 +29,8 @@
 import org.springframework.amqp.core.MessageProperties;
 import org.springframework.amqp.core.MessagePropertiesBuilder;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 /**
  * @author Gary Russell
  * @author Alex Panchenko
diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/AmqpMessageHeaderAccessorTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/AmqpMessageHeaderAccessorTests.java
index 3368346e5a..6a7d2b009c 100644
--- a/spring-amqp/src/test/java/org/springframework/amqp/support/AmqpMessageHeaderAccessorTests.java
+++ b/spring-amqp/src/test/java/org/springframework/amqp/support/AmqpMessageHeaderAccessorTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -16,9 +16,6 @@
 
 package org.springframework.amqp.support;
 
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
-
 import java.util.Date;
 import java.util.Map;
 
@@ -30,6 +27,9 @@
 import org.springframework.messaging.support.MessageBuilder;
 import org.springframework.util.MimeType;
 
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
 /**
  * @author Stephane Nicoll
  * @author Gary Russell
diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/MessagePostProcessorUtilsTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/MessagePostProcessorUtilsTests.java
index 563c2b1db9..7a28939d39 100644
--- a/spring-amqp/src/test/java/org/springframework/amqp/support/MessagePostProcessorUtilsTests.java
+++ b/spring-amqp/src/test/java/org/springframework/amqp/support/MessagePostProcessorUtilsTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2014-2019 the original author or authors.
+ * Copyright 2014-2025 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.
@@ -16,8 +16,6 @@
 
 package org.springframework.amqp.support;
 
-import static org.assertj.core.api.Assertions.assertThat;
-
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Iterator;
@@ -31,6 +29,8 @@
 import org.springframework.core.Ordered;
 import org.springframework.core.PriorityOrdered;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 /**
  * @author Gary Russell
  * @since 1.4.2
diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/SimpleAmqpHeaderMapperTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/SimpleAmqpHeaderMapperTests.java
index 2e4d846783..b39a0eccf5 100644
--- a/spring-amqp/src/test/java/org/springframework/amqp/support/SimpleAmqpHeaderMapperTests.java
+++ b/spring-amqp/src/test/java/org/springframework/amqp/support/SimpleAmqpHeaderMapperTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -16,10 +16,6 @@
 
 package org.springframework.amqp.support;
 
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.assertj.core.api.Assertions.fail;
-
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
@@ -34,6 +30,10 @@
 import org.springframework.messaging.MessageHeaders;
 import org.springframework.util.MimeTypeUtils;
 
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.fail;
+
 /**
  * @author Mark Fisher
  * @author Gary Russell
diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/AllowedListDeserializingMessageConverterTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/AllowedListDeserializingMessageConverterTests.java
index 573784c1b5..32d96caf02 100644
--- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/AllowedListDeserializingMessageConverterTests.java
+++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/AllowedListDeserializingMessageConverterTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016-2024 the original author or authors.
+ * Copyright 2016-2025 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.
@@ -16,9 +16,6 @@
 
 package org.springframework.amqp.support.converter;
 
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
-
 import java.io.Serializable;
 import java.util.Collections;
 
@@ -28,6 +25,9 @@
 import org.springframework.amqp.core.MessageProperties;
 import org.springframework.util.Assert;
 
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
 /**
  * @author Gary Russell
  * @since 1.5.5
diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverterTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverterTests.java
index 731d92546a..58598b0ff7 100644
--- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverterTests.java
+++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverterTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015-2019 the original author or authors.
+ * Copyright 2015-2025 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.
@@ -16,9 +16,6 @@
 
 package org.springframework.amqp.support.converter;
 
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.fail;
-
 import java.io.Serializable;
 
 import org.junit.jupiter.api.Test;
@@ -26,6 +23,9 @@
 import org.springframework.amqp.core.Message;
 import org.springframework.amqp.core.MessageProperties;
 
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+
 /**
  * @author Gary Russell
  * @author Artem Bilan
diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/DefaultClassMapperTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/DefaultClassMapperTests.java
index 74e90ca538..941feddbaf 100644
--- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/DefaultClassMapperTests.java
+++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/DefaultClassMapperTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -16,9 +16,6 @@
 
 package org.springframework.amqp.support.converter;
 
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.BDDMockito.given;
-
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.Map;
@@ -30,6 +27,9 @@
 
 import org.springframework.amqp.core.MessageProperties;
 
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.given;
+
 /**
  * @author James Carr
  * @author Gary Russell
diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapperTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapperTests.java
index 4f2f43ed49..348b93fbfa 100644
--- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapperTests.java
+++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapperTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -16,14 +16,14 @@
 
 package org.springframework.amqp.support.converter;
 
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.fail;
-import static org.mockito.BDDMockito.given;
-
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Map;
 
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.type.CollectionType;
+import com.fasterxml.jackson.databind.type.MapType;
+import com.fasterxml.jackson.databind.type.TypeFactory;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -32,10 +32,9 @@
 
 import org.springframework.amqp.core.MessageProperties;
 
-import com.fasterxml.jackson.databind.JavaType;
-import com.fasterxml.jackson.databind.type.CollectionType;
-import com.fasterxml.jackson.databind.type.MapType;
-import com.fasterxml.jackson.databind.type.TypeFactory;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+import static org.mockito.BDDMockito.given;
 
 /**
  * @author James Carr
diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverterTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverterTests.java
index 574fecc06f..51e7d7ef2f 100644
--- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverterTests.java
+++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverterTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -16,9 +16,6 @@
 
 package org.springframework.amqp.support.converter;
 
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
-
 import java.io.IOException;
 import java.math.BigDecimal;
 import java.util.Hashtable;
@@ -26,6 +23,13 @@
 import java.util.List;
 import java.util.Map;
 
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.databind.ser.BeanSerializerFactory;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
@@ -38,13 +42,8 @@
 import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
 import org.springframework.util.MimeTypeUtils;
 
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.DeserializationContext;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
-import com.fasterxml.jackson.databind.module.SimpleModule;
-import com.fasterxml.jackson.databind.ser.BeanSerializerFactory;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 
 /**
  * @author Mark Pollack
diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2XmlMessageConverterTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2XmlMessageConverterTests.java
index 836ee45ca0..b1a97acf75 100644
--- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2XmlMessageConverterTests.java
+++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2XmlMessageConverterTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-2024 the original author or authors.
+ * Copyright 2018-2025 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.
@@ -16,14 +16,14 @@
 
 package org.springframework.amqp.support.converter;
 
-import static org.assertj.core.api.Assertions.assertThat;
-
 import java.math.BigDecimal;
 import java.util.Hashtable;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
+import com.fasterxml.jackson.databind.ser.BeanSerializerFactory;
+import com.fasterxml.jackson.dataformat.xml.XmlMapper;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
@@ -34,8 +34,7 @@
 import org.springframework.test.annotation.DirtiesContext;
 import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
 
-import com.fasterxml.jackson.databind.ser.BeanSerializerFactory;
-import com.fasterxml.jackson.dataformat.xml.XmlMapper;
+import static org.assertj.core.api.Assertions.assertThat;
 
 /**
  * @author Mohammad Hewedy
diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/MarshallingMessageConverterTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/MarshallingMessageConverterTests.java
index bae3eb14d9..70d8972109 100644
--- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/MarshallingMessageConverterTests.java
+++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/MarshallingMessageConverterTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -16,8 +16,6 @@
 
 package org.springframework.amqp.support.converter;
 
-import static org.assertj.core.api.Assertions.assertThat;
-
 import java.io.IOException;
 
 import javax.xml.transform.Result;
@@ -33,6 +31,8 @@
 import org.springframework.oxm.Unmarshaller;
 import org.springframework.oxm.XmlMappingException;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 /**
  * @author Mark Fisher
  * @author James Carr
diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/MessagingMessageConverterTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/MessagingMessageConverterTests.java
index 3482f75b02..5bd63e1541 100644
--- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/MessagingMessageConverterTests.java
+++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/MessagingMessageConverterTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -16,15 +16,15 @@
 
 package org.springframework.amqp.support.converter;
 
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
-
 import org.junit.jupiter.api.Test;
 
 import org.springframework.amqp.core.MessageProperties;
 import org.springframework.messaging.Message;
 import org.springframework.messaging.support.MessageBuilder;
 
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
 
 /**
  * @author Stephane Nicoll
diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/SerializerMessageConverterTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/SerializerMessageConverterTests.java
index 026fe7a308..9ec8889118 100644
--- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/SerializerMessageConverterTests.java
+++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/SerializerMessageConverterTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -16,9 +16,6 @@
 
 package org.springframework.amqp.support.converter;
 
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -31,6 +28,9 @@
 import org.springframework.amqp.core.Message;
 import org.springframework.amqp.core.MessageProperties;
 
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
 /**
  * @author Mark Fisher
  * @author Gary Russell
diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/SimpleMessageConverterTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/SimpleMessageConverterTests.java
index ec6e72d9fb..e491ae377d 100644
--- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/SimpleMessageConverterTests.java
+++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/SimpleMessageConverterTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -16,10 +16,6 @@
 
 package org.springframework.amqp.support.converter;
 
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.assertj.core.api.Assertions.fail;
-
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.ObjectInputStream;
@@ -30,6 +26,10 @@
 import org.springframework.amqp.core.Message;
 import org.springframework.amqp.core.MessageProperties;
 
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.fail;
+
 /**
  * @author Mark Fisher
  * @author Gary Russell
diff --git a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunning.java b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunning.java
index 1f9ba0b9df..86dea5aefb 100644
--- a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunning.java
+++ b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunning.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -16,11 +16,9 @@
 
 package org.springframework.amqp.rabbit.junit;
 
-import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeNoException;
-
 import java.util.Map;
 
+import com.rabbitmq.client.ConnectionFactory;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.junit.rules.TestWatcher;
@@ -29,7 +27,8 @@
 
 import org.springframework.amqp.rabbit.junit.BrokerRunningSupport.BrokerNotAliveException;
 
-import com.rabbitmq.client.ConnectionFactory;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeNoException;
 
 /**
  * A rule that prevents integration tests from failing if the Rabbit broker application is
diff --git a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java
index ebd5414c97..3fdce94135 100644
--- a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java
+++ b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2023 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -36,6 +36,9 @@
 import java.util.UUID;
 import java.util.concurrent.TimeoutException;
 
+import com.rabbitmq.client.Channel;
+import com.rabbitmq.client.Connection;
+import com.rabbitmq.client.ConnectionFactory;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
@@ -43,10 +46,6 @@
 import org.springframework.util.StringUtils;
 import org.springframework.web.util.UriUtils;
 
-import com.rabbitmq.client.Channel;
-import com.rabbitmq.client.Connection;
-import com.rabbitmq.client.ConnectionFactory;
-
 /**
  * A class that can be used to prevent integration tests from failing if the Rabbit broker application is
  * not running or not accessible. If the Rabbit broker is not running in the background
diff --git a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/RabbitAvailableCondition.java b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/RabbitAvailableCondition.java
index 5849667783..bbb0fa4704 100644
--- a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/RabbitAvailableCondition.java
+++ b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/RabbitAvailableCondition.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2017-2019 the original author or authors.
+ * Copyright 2017-2025 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.
@@ -19,6 +19,7 @@
 import java.lang.reflect.AnnotatedElement;
 import java.util.Optional;
 
+import com.rabbitmq.client.ConnectionFactory;
 import org.junit.jupiter.api.extension.AfterAllCallback;
 import org.junit.jupiter.api.extension.AfterEachCallback;
 import org.junit.jupiter.api.extension.ConditionEvaluationResult;
@@ -33,8 +34,6 @@
 import org.springframework.core.annotation.MergedAnnotations;
 import org.springframework.util.Assert;
 
-import com.rabbitmq.client.ConnectionFactory;
-
 /**
  * JUnit5 {@link ExecutionCondition}. Looks for {@code @RabbitAvailable} annotated classes
  * and disables if found the broker is not available.
diff --git a/spring-rabbit-junit/src/test/java/org/springframework/amqp/rabbit/junit/BrokerRunningTests.java b/spring-rabbit-junit/src/test/java/org/springframework/amqp/rabbit/junit/BrokerRunningTests.java
index 60e73344d7..0f387ecc57 100644
--- a/spring-rabbit-junit/src/test/java/org/springframework/amqp/rabbit/junit/BrokerRunningTests.java
+++ b/spring-rabbit-junit/src/test/java/org/springframework/amqp/rabbit/junit/BrokerRunningTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2017-2019 the original author or authors.
+ * Copyright 2017-2025 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.
@@ -16,16 +16,15 @@
 
 package org.springframework.amqp.rabbit.junit;
 
-import static org.assertj.core.api.Assertions.assertThat;
-
 import java.util.HashMap;
 import java.util.Map;
 
+import com.rabbitmq.client.ConnectionFactory;
 import org.junit.jupiter.api.Test;
 
 import org.springframework.beans.DirectFieldAccessor;
 
-import com.rabbitmq.client.ConnectionFactory;
+import static org.assertj.core.api.Assertions.assertThat;
 
 /**
  * @author Gary Russell
diff --git a/spring-rabbit-junit/src/test/java/org/springframework/amqp/rabbit/junit/RabbitAvailableCTORInjectionTests.java b/spring-rabbit-junit/src/test/java/org/springframework/amqp/rabbit/junit/RabbitAvailableCTORInjectionTests.java
index 55f643f288..84ea5af2c8 100644
--- a/spring-rabbit-junit/src/test/java/org/springframework/amqp/rabbit/junit/RabbitAvailableCTORInjectionTests.java
+++ b/spring-rabbit-junit/src/test/java/org/springframework/amqp/rabbit/junit/RabbitAvailableCTORInjectionTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2017-2019 the original author or authors.
+ * Copyright 2017-2025 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.
@@ -16,14 +16,13 @@
 
 package org.springframework.amqp.rabbit.junit;
 
-import static org.assertj.core.api.Assertions.assertThat;
-
-import org.junit.jupiter.api.Test;
-
 import com.rabbitmq.client.AMQP.Queue.DeclareOk;
 import com.rabbitmq.client.Channel;
 import com.rabbitmq.client.Connection;
 import com.rabbitmq.client.ConnectionFactory;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
 
 /**
  * @author Gary Russell
diff --git a/spring-rabbit-junit/src/test/java/org/springframework/amqp/rabbit/junit/RabbitAvailableTests.java b/spring-rabbit-junit/src/test/java/org/springframework/amqp/rabbit/junit/RabbitAvailableTests.java
index d23b0190a5..9dedf0a7f4 100644
--- a/spring-rabbit-junit/src/test/java/org/springframework/amqp/rabbit/junit/RabbitAvailableTests.java
+++ b/spring-rabbit-junit/src/test/java/org/springframework/amqp/rabbit/junit/RabbitAvailableTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2017-2022 the original author or authors.
+ * Copyright 2017-2025 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.
@@ -16,14 +16,13 @@
 
 package org.springframework.amqp.rabbit.junit;
 
-import static org.assertj.core.api.Assertions.assertThat;
-
-import org.junit.jupiter.api.Test;
-
 import com.rabbitmq.client.AMQP.Queue.DeclareOk;
 import com.rabbitmq.client.Channel;
 import com.rabbitmq.client.Connection;
 import com.rabbitmq.client.ConnectionFactory;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
 
 /**
  * @author Gary Russell
diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/StreamRabbitListenerContainerFactory.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/StreamRabbitListenerContainerFactory.java
index a589f6ffd1..6911571179 100644
--- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/StreamRabbitListenerContainerFactory.java
+++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/StreamRabbitListenerContainerFactory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021-2024 the original author or authors.
+ * Copyright 2021-2025 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.
@@ -18,6 +18,7 @@
 
 import java.lang.reflect.Method;
 
+import com.rabbitmq.stream.Environment;
 import org.aopalliance.aop.Advice;
 
 import org.springframework.amqp.rabbit.batch.BatchingStrategy;
@@ -34,8 +35,6 @@
 import org.springframework.rabbit.stream.micrometer.RabbitStreamListenerObservationConvention;
 import org.springframework.util.Assert;
 
-import com.rabbitmq.stream.Environment;
-
 /**
  * Factory for StreamListenerContainer.
  *
diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java
index 842ca8f071..a4d3706719 100644
--- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java
+++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021-2024 the original author or authors.
+ * Copyright 2021-2025 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.
@@ -22,6 +22,12 @@
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 
+import com.rabbitmq.stream.Codec;
+import com.rabbitmq.stream.Consumer;
+import com.rabbitmq.stream.ConsumerBuilder;
+import com.rabbitmq.stream.Environment;
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationRegistry;
 import org.aopalliance.aop.Advice;
 import org.apache.commons.logging.LogFactory;
 
@@ -44,13 +50,6 @@
 import org.springframework.rabbit.stream.support.converter.StreamMessageConverter;
 import org.springframework.util.Assert;
 
-import com.rabbitmq.stream.Codec;
-import com.rabbitmq.stream.Consumer;
-import com.rabbitmq.stream.ConsumerBuilder;
-import com.rabbitmq.stream.Environment;
-import io.micrometer.observation.Observation;
-import io.micrometer.observation.ObservationRegistry;
-
 /**
  * A listener container for RabbitMQ Streams.
  *
diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamMessageListener.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamMessageListener.java
index 679f6525f2..738b08cb62 100644
--- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamMessageListener.java
+++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamMessageListener.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021 the original author or authors.
+ * Copyright 2021-2025 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.
@@ -16,11 +16,11 @@
 
 package org.springframework.rabbit.stream.listener;
 
-import org.springframework.amqp.core.MessageListener;
-
 import com.rabbitmq.stream.Message;
 import com.rabbitmq.stream.MessageHandler.Context;
 
+import org.springframework.amqp.core.MessageListener;
+
 /**
  * A message listener that receives native stream messages.
  *
diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/adapter/StreamMessageListenerAdapter.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/adapter/StreamMessageListenerAdapter.java
index 359f3c6559..30748291b6 100644
--- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/adapter/StreamMessageListenerAdapter.java
+++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/adapter/StreamMessageListenerAdapter.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021-2022 the original author or authors.
+ * Copyright 2021-2025 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.
@@ -18,15 +18,15 @@
 
 import java.lang.reflect.Method;
 
+import com.rabbitmq.stream.Message;
+import com.rabbitmq.stream.MessageHandler.Context;
+
 import org.springframework.amqp.rabbit.listener.adapter.InvocationResult;
 import org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter;
 import org.springframework.amqp.rabbit.listener.api.RabbitListenerErrorHandler;
 import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException;
 import org.springframework.rabbit.stream.listener.StreamMessageListener;
 
-import com.rabbitmq.stream.Message;
-import com.rabbitmq.stream.MessageHandler.Context;
-
 /**
  * A listener adapter that receives native stream messages.
  *
diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamMessageReceiverContext.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamMessageReceiverContext.java
index 8cc33b7bc5..c2a2656860 100644
--- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamMessageReceiverContext.java
+++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamMessageReceiverContext.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 the original author or authors.
+ * Copyright 2023-2025 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.
@@ -19,14 +19,14 @@
 import java.nio.charset.StandardCharsets;
 import java.util.Map;
 
-import org.springframework.amqp.rabbit.support.micrometer.RabbitListenerObservation;
-import org.springframework.amqp.rabbit.support.micrometer.RabbitListenerObservationConvention;
-import org.springframework.amqp.rabbit.support.micrometer.RabbitMessageReceiverContext;
-
 import com.rabbitmq.stream.Message;
 import io.micrometer.common.KeyValues;
 import io.micrometer.observation.transport.ReceiverContext;
 
+import org.springframework.amqp.rabbit.support.micrometer.RabbitListenerObservation;
+import org.springframework.amqp.rabbit.support.micrometer.RabbitListenerObservationConvention;
+import org.springframework.amqp.rabbit.support.micrometer.RabbitMessageReceiverContext;
+
 /**
  * {@link ReceiverContext} for stream {@link Message}s.
  *
diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamTemplateObservation.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamTemplateObservation.java
index cd095f0617..777031ce48 100644
--- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamTemplateObservation.java
+++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamTemplateObservation.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 the original author or authors.
+ * Copyright 2023-2025 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.
@@ -16,14 +16,14 @@
 
 package org.springframework.rabbit.stream.micrometer;
 
-import org.springframework.rabbit.stream.producer.RabbitStreamTemplate;
-
 import io.micrometer.common.KeyValues;
 import io.micrometer.common.docs.KeyName;
 import io.micrometer.observation.Observation.Context;
 import io.micrometer.observation.ObservationConvention;
 import io.micrometer.observation.docs.ObservationDocumentation;
 
+import org.springframework.rabbit.stream.producer.RabbitStreamTemplate;
+
 /**
  * Spring RabbitMQ Observation for
  * {@link org.springframework.rabbit.stream.producer.RabbitStreamTemplate}.
diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations.java
index bdd2eae26d..80977ac8b0 100644
--- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations.java
+++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021 the original author or authors.
+ * Copyright 2021-2025 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.
@@ -18,6 +18,8 @@
 
 import java.util.concurrent.CompletableFuture;
 
+import com.rabbitmq.stream.MessageBuilder;
+
 import org.springframework.amqp.AmqpException;
 import org.springframework.amqp.core.Message;
 import org.springframework.amqp.core.MessagePostProcessor;
@@ -25,8 +27,6 @@
 import org.springframework.lang.Nullable;
 import org.springframework.rabbit.stream.support.converter.StreamMessageConverter;
 
-import com.rabbitmq.stream.MessageBuilder;
-
 /**
  * Provides methods for sending messages using a RabbitMQ Stream producer.
  *
diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java
index a0bde53ad2..b2bde13775 100644
--- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java
+++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021-2024 the original author or authors.
+ * Copyright 2021-2025 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.
@@ -21,6 +21,15 @@
 import java.util.concurrent.locks.ReentrantLock;
 import java.util.function.Function;
 
+import com.rabbitmq.stream.ConfirmationHandler;
+import com.rabbitmq.stream.Constants;
+import com.rabbitmq.stream.Environment;
+import com.rabbitmq.stream.MessageBuilder;
+import com.rabbitmq.stream.Producer;
+import com.rabbitmq.stream.ProducerBuilder;
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationRegistry;
+
 import org.springframework.amqp.core.Message;
 import org.springframework.amqp.core.MessagePostProcessor;
 import org.springframework.amqp.support.converter.MessageConverter;
@@ -41,15 +50,6 @@
 import org.springframework.rabbit.stream.support.converter.StreamMessageConverter;
 import org.springframework.util.Assert;
 
-import com.rabbitmq.stream.ConfirmationHandler;
-import com.rabbitmq.stream.Constants;
-import com.rabbitmq.stream.Environment;
-import com.rabbitmq.stream.MessageBuilder;
-import com.rabbitmq.stream.Producer;
-import com.rabbitmq.stream.ProducerBuilder;
-import io.micrometer.observation.Observation;
-import io.micrometer.observation.ObservationRegistry;
-
 /**
  * Default implementation of {@link RabbitStreamOperations}.
  *
diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/StreamMessageRecoverer.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/StreamMessageRecoverer.java
index 222a5a215c..bc890b548f 100644
--- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/StreamMessageRecoverer.java
+++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/StreamMessageRecoverer.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022 the original author or authors.
+ * Copyright 2022-2025 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.
@@ -16,11 +16,11 @@
 
 package org.springframework.rabbit.stream.retry;
 
+import com.rabbitmq.stream.MessageHandler.Context;
+
 import org.springframework.amqp.core.Message;
 import org.springframework.amqp.rabbit.retry.MessageRecoverer;
 
-import com.rabbitmq.stream.MessageHandler.Context;
-
 /**
  * Implementations of this interface can handle failed messages after retries are
  * exhausted.
diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/StreamRetryOperationsInterceptorFactoryBean.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/StreamRetryOperationsInterceptorFactoryBean.java
index e4960b4bb8..3662cf04a4 100644
--- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/StreamRetryOperationsInterceptorFactoryBean.java
+++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/StreamRetryOperationsInterceptorFactoryBean.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022 the original author or authors.
+ * Copyright 2022-2025 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.
@@ -16,6 +16,9 @@
 
 package org.springframework.rabbit.stream.retry;
 
+import com.rabbitmq.stream.Message;
+import com.rabbitmq.stream.MessageHandler.Context;
+
 import org.springframework.amqp.rabbit.config.StatelessRetryOperationsInterceptorFactoryBean;
 import org.springframework.amqp.rabbit.retry.MessageRecoverer;
 import org.springframework.rabbit.stream.listener.StreamListenerContainer;
@@ -23,9 +26,6 @@
 import org.springframework.retry.interceptor.MethodInvocationRecoverer;
 import org.springframework.retry.support.RetryTemplate;
 
-import com.rabbitmq.stream.Message;
-import com.rabbitmq.stream.MessageHandler.Context;
-
 /**
  * Convenient factory bean for creating a stateless retry interceptor for use in a
  * {@link StreamListenerContainer} when consuming native stream messages, giving you a
diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/StreamAdmin.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/StreamAdmin.java
index dc6e336a7a..4d9211551c 100644
--- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/StreamAdmin.java
+++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/StreamAdmin.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 the original author or authors.
+ * Copyright 2023-2025 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.
@@ -18,12 +18,12 @@
 
 import java.util.function.Consumer;
 
-import org.springframework.context.SmartLifecycle;
-import org.springframework.util.Assert;
-
 import com.rabbitmq.stream.Environment;
 import com.rabbitmq.stream.StreamCreator;
 
+import org.springframework.context.SmartLifecycle;
+import org.springframework.util.Assert;
+
 /**
  * Used to provision streams.
  *
diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/StreamMessageProperties.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/StreamMessageProperties.java
index 23e170f781..36eb6d0c57 100644
--- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/StreamMessageProperties.java
+++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/StreamMessageProperties.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021 the original author or authors.
+ * Copyright 2021-2025 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.
@@ -18,11 +18,11 @@
 
 import java.util.Objects;
 
+import com.rabbitmq.stream.MessageHandler.Context;
+
 import org.springframework.amqp.core.MessageProperties;
 import org.springframework.lang.Nullable;
 
-import com.rabbitmq.stream.MessageHandler.Context;
-
 /**
  * {@link MessageProperties} extension for stream messages.
  *
diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverter.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverter.java
index 614f4a6e79..b50c797a17 100644
--- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverter.java
+++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverter.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021-2024 the original author or authors.
+ * Copyright 2021-2025 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.
@@ -22,6 +22,13 @@
 import java.util.UUID;
 import java.util.function.Supplier;
 
+import com.rabbitmq.stream.Codec;
+import com.rabbitmq.stream.MessageBuilder;
+import com.rabbitmq.stream.MessageBuilder.ApplicationPropertiesBuilder;
+import com.rabbitmq.stream.MessageBuilder.PropertiesBuilder;
+import com.rabbitmq.stream.Properties;
+import com.rabbitmq.stream.codec.WrapperMessageBuilder;
+
 import org.springframework.amqp.core.Message;
 import org.springframework.amqp.core.MessageProperties;
 import org.springframework.amqp.support.converter.MessageConversionException;
@@ -30,13 +37,6 @@
 import org.springframework.rabbit.stream.support.StreamMessageProperties;
 import org.springframework.util.Assert;
 
-import com.rabbitmq.stream.Codec;
-import com.rabbitmq.stream.MessageBuilder;
-import com.rabbitmq.stream.MessageBuilder.ApplicationPropertiesBuilder;
-import com.rabbitmq.stream.MessageBuilder.PropertiesBuilder;
-import com.rabbitmq.stream.Properties;
-import com.rabbitmq.stream.codec.WrapperMessageBuilder;
-
 /**
  * Default {@link StreamMessageConverter}.
  *
diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamProvisioningTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamProvisioningTests.java
index 75880c5e2d..d616d4a25e 100644
--- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamProvisioningTests.java
+++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/config/SuperStreamProvisioningTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022-2024 the original author or authors.
+ * Copyright 2022-2025 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.
@@ -16,8 +16,6 @@
 
 package org.springframework.rabbit.stream.config;
 
-import static org.assertj.core.api.Assertions.assertThat;
-
 import java.util.List;
 
 import org.junit.jupiter.api.Test;
@@ -35,6 +33,8 @@
 import org.springframework.test.annotation.DirtiesContext;
 import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 /**
  * @author Gary Russell
  * @since 3.0
diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java
index c418c0ecfd..be41f1593e 100644
--- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java
+++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021-2023 the original author or authors.
+ * Copyright 2021-2025 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.
@@ -16,8 +16,6 @@
 
 package org.springframework.rabbit.stream.listener;
 
-import static org.assertj.core.api.Assertions.assertThat;
-
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.nio.charset.StandardCharsets;
@@ -30,6 +28,16 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 
+import com.rabbitmq.stream.Environment;
+import com.rabbitmq.stream.Message;
+import com.rabbitmq.stream.MessageHandler.Context;
+import com.rabbitmq.stream.OffsetSpecification;
+import io.micrometer.common.KeyValues;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler;
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+import io.micrometer.core.tck.MeterRegistryAssert;
+import io.micrometer.observation.ObservationRegistry;
 import org.junit.jupiter.api.Test;
 
 import org.springframework.amqp.core.Queue;
@@ -61,16 +69,7 @@
 import org.springframework.web.reactive.function.client.WebClient;
 import org.springframework.web.util.UriUtils;
 
-import com.rabbitmq.stream.Environment;
-import com.rabbitmq.stream.Message;
-import com.rabbitmq.stream.MessageHandler.Context;
-import com.rabbitmq.stream.OffsetSpecification;
-import io.micrometer.common.KeyValues;
-import io.micrometer.core.instrument.MeterRegistry;
-import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler;
-import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
-import io.micrometer.core.tck.MeterRegistryAssert;
-import io.micrometer.observation.ObservationRegistry;
+import static org.assertj.core.api.Assertions.assertThat;
 
 /**
  * @author Gary Russell
diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/StreamListenerContainerTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/StreamListenerContainerTests.java
index 3c681f62cc..2b8cb7092b 100644
--- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/StreamListenerContainerTests.java
+++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/StreamListenerContainerTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022 the original author or authors.
+ * Copyright 2022-2025 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.
@@ -16,27 +16,26 @@
 
 package org.springframework.rabbit.stream.listener;
 
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.isNull;
-import static org.mockito.BDDMockito.given;
-import static org.mockito.BDDMockito.willAnswer;
-import static org.mockito.Mockito.mock;
-
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
 
+import com.rabbitmq.stream.ConsumerBuilder;
+import com.rabbitmq.stream.Environment;
+import com.rabbitmq.stream.Message;
+import com.rabbitmq.stream.MessageHandler;
+import com.rabbitmq.stream.MessageHandler.Context;
 import org.aopalliance.intercept.MethodInterceptor;
 import org.junit.jupiter.api.Test;
 
 import org.springframework.amqp.core.MessageListener;
 import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
 
-import com.rabbitmq.stream.ConsumerBuilder;
-import com.rabbitmq.stream.Environment;
-import com.rabbitmq.stream.Message;
-import com.rabbitmq.stream.MessageHandler;
-import com.rabbitmq.stream.MessageHandler.Context;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.willAnswer;
+import static org.mockito.Mockito.mock;
 
 /**
  * @author Gary Russell
diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamConcurrentSACTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamConcurrentSACTests.java
index 50e1ff1058..0ca49fb872 100644
--- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamConcurrentSACTests.java
+++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamConcurrentSACTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022-2024 the original author or authors.
+ * Copyright 2022-2025 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.
@@ -16,13 +16,13 @@
 
 package org.springframework.rabbit.stream.listener;
 
-import static org.assertj.core.api.Assertions.assertThat;
-
 import java.util.HashSet;
 import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
+import com.rabbitmq.stream.Environment;
+import com.rabbitmq.stream.OffsetSpecification;
 import org.junit.jupiter.api.Test;
 
 import org.springframework.amqp.core.Declarables;
@@ -40,8 +40,7 @@
 import org.springframework.test.annotation.DirtiesContext;
 import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
 
-import com.rabbitmq.stream.Environment;
-import com.rabbitmq.stream.OffsetSpecification;
+import static org.assertj.core.api.Assertions.assertThat;
 
 /**
  * @author Gary Russell
diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java
index a474f6926e..f14a3947dd 100644
--- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java
+++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/SuperStreamSACTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022-2024 the original author or authors.
+ * Copyright 2022-2025 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.
@@ -16,8 +16,6 @@
 
 package org.springframework.rabbit.stream.listener;
 
-import static org.assertj.core.api.Assertions.assertThat;
-
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -27,6 +25,8 @@
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 
+import com.rabbitmq.stream.Environment;
+import com.rabbitmq.stream.OffsetSpecification;
 import org.junit.jupiter.api.Test;
 
 import org.springframework.amqp.core.Declarables;
@@ -48,8 +48,7 @@
 import org.springframework.test.annotation.DirtiesContext;
 import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
 
-import com.rabbitmq.stream.Environment;
-import com.rabbitmq.stream.OffsetSpecification;
+import static org.assertj.core.api.Assertions.assertThat;
 
 /**
  * @author Gary Russell
diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/micrometer/TracingTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/micrometer/TracingTests.java
index a90eb4ac7c..14654159a5 100644
--- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/micrometer/TracingTests.java
+++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/micrometer/TracingTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2023 the original author or authors.
+ * Copyright 2023-2025 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.
@@ -16,13 +16,20 @@
 
 package org.springframework.rabbit.stream.micrometer;
 
-import static org.assertj.core.api.Assertions.assertThat;
-
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
+import com.rabbitmq.stream.Environment;
+import com.rabbitmq.stream.Message;
+import com.rabbitmq.stream.OffsetSpecification;
+import io.micrometer.observation.ObservationRegistry;
+import io.micrometer.tracing.Span.Kind;
+import io.micrometer.tracing.exporter.FinishedSpan;
+import io.micrometer.tracing.test.SampleTestRunner;
+import io.micrometer.tracing.test.simple.SpanAssert;
+import io.micrometer.tracing.test.simple.SpansAssert;
 import org.testcontainers.junit.jupiter.Testcontainers;
 
 import org.springframework.amqp.rabbit.annotation.EnableRabbit;
@@ -38,15 +45,7 @@
 import org.springframework.rabbit.stream.producer.RabbitStreamTemplate;
 import org.springframework.rabbit.stream.support.StreamAdmin;
 
-import com.rabbitmq.stream.Environment;
-import com.rabbitmq.stream.Message;
-import com.rabbitmq.stream.OffsetSpecification;
-import io.micrometer.observation.ObservationRegistry;
-import io.micrometer.tracing.Span.Kind;
-import io.micrometer.tracing.exporter.FinishedSpan;
-import io.micrometer.tracing.test.SampleTestRunner;
-import io.micrometer.tracing.test.simple.SpanAssert;
-import io.micrometer.tracing.test.simple.SpansAssert;
+import static org.assertj.core.api.Assertions.assertThat;
 
 /**
  * @author Gary Russell
diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplateTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplateTests.java
index 9d06f42abe..f1dd52d432 100644
--- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplateTests.java
+++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplateTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022 the original author or authors.
+ * Copyright 2022-2025 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.
@@ -16,24 +16,10 @@
 
 package org.springframework.rabbit.stream.producer;
 
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.BDDMockito.given;
-import static org.mockito.BDDMockito.willAnswer;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.atomic.AtomicInteger;
 
-import org.junit.jupiter.api.Test;
-
-import org.springframework.amqp.support.converter.SimpleMessageConverter;
-import org.springframework.rabbit.stream.support.converter.StreamMessageConverter;
-
 import com.rabbitmq.stream.ConfirmationHandler;
 import com.rabbitmq.stream.ConfirmationStatus;
 import com.rabbitmq.stream.Constants;
@@ -41,6 +27,19 @@
 import com.rabbitmq.stream.Message;
 import com.rabbitmq.stream.Producer;
 import com.rabbitmq.stream.ProducerBuilder;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.amqp.support.converter.SimpleMessageConverter;
+import org.springframework.rabbit.stream.support.converter.StreamMessageConverter;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.willAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
 
 /**
  * @author Gary Russell
diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverterTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverterTests.java
index 342e138302..c63d682f4e 100644
--- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverterTests.java
+++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverterTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021 the original author or authors.
+ * Copyright 2021-2025 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.
@@ -16,16 +16,15 @@
 
 package org.springframework.rabbit.stream.support.converter;
 
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
-
+import com.rabbitmq.stream.MessageHandler.Context;
+import com.rabbitmq.stream.Properties;
 import org.junit.jupiter.api.Test;
 
 import org.springframework.amqp.core.Message;
 import org.springframework.rabbit.stream.support.StreamMessageProperties;
 
-import com.rabbitmq.stream.MessageHandler.Context;
-import com.rabbitmq.stream.Properties;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
 
 /**
  * @author Gary Russell
diff --git a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/TestRabbitTemplate.java b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/TestRabbitTemplate.java
index dd35f91a41..d9ac0ee452 100644
--- a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/TestRabbitTemplate.java
+++ b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/TestRabbitTemplate.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2017-2024 the original author or authors.
+ * Copyright 2017-2025 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.
@@ -16,12 +16,6 @@
 
 package org.springframework.amqp.rabbit.test;
 
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.BDDMockito.willAnswer;
-import static org.mockito.Mockito.mock;
-
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -33,6 +27,10 @@
 import java.util.concurrent.locks.ReentrantLock;
 import java.util.stream.Stream;
 
+import com.rabbitmq.client.AMQP.BasicProperties;
+import com.rabbitmq.client.Channel;
+import com.rabbitmq.client.Envelope;
+
 import org.springframework.amqp.core.Message;
 import org.springframework.amqp.core.MessageBuilder;
 import org.springframework.amqp.core.MessageListener;
@@ -51,9 +49,11 @@
 import org.springframework.context.ApplicationListener;
 import org.springframework.context.event.ContextRefreshedEvent;
 
-import com.rabbitmq.client.AMQP.BasicProperties;
-import com.rabbitmq.client.Channel;
-import com.rabbitmq.client.Envelope;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.BDDMockito.willAnswer;
+import static org.mockito.Mockito.mock;
 
 /**
  * A {@link RabbitTemplate} that invokes {@code @RabbitListener} s directly.
diff --git a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/repeatable/AbstractRabbitAnnotationDrivenTests.java b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/repeatable/AbstractRabbitAnnotationDrivenTests.java
index 289d230bce..9b826d6492 100644
--- a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/repeatable/AbstractRabbitAnnotationDrivenTests.java
+++ b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/repeatable/AbstractRabbitAnnotationDrivenTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016-2019 the original author or authors.
+ * Copyright 2016-2025 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.
@@ -16,8 +16,6 @@
 
 package org.springframework.amqp.rabbit.repeatable;
 
-import static org.assertj.core.api.Assertions.assertThat;
-
 import org.junit.jupiter.api.Test;
 
 import org.springframework.amqp.rabbit.annotation.RabbitHandler;
@@ -27,6 +25,8 @@
 import org.springframework.context.ApplicationContext;
 import org.springframework.stereotype.Component;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 /**
  *
  * @author Stephane Nicoll
diff --git a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/RabbitListenerProxyTest.java b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/RabbitListenerProxyTest.java
index 20395cedb7..cc89938ef7 100644
--- a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/RabbitListenerProxyTest.java
+++ b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/RabbitListenerProxyTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 the original author or authors.
+ * Copyright 2020-2025 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.
@@ -16,11 +16,6 @@
 
 package org.springframework.amqp.rabbit.test;
 
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.BDDMockito.willAnswer;
-import static org.mockito.Mockito.verify;
-
 import org.junit.jupiter.api.Test;
 
 import org.springframework.amqp.core.AnonymousQueue;
@@ -41,6 +36,11 @@
 import org.springframework.test.annotation.DirtiesContext;
 import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
 
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.BDDMockito.willAnswer;
+import static org.mockito.Mockito.verify;
+
 /**
  * @author Miguel Gross Valle
  *
diff --git a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/context/SpringRabbitTestTests.java b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/context/SpringRabbitTestTests.java
index 697e984a0f..8a2ebf65a7 100644
--- a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/context/SpringRabbitTestTests.java
+++ b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/context/SpringRabbitTestTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2024 the original author or authors.
+ * Copyright 2020-2025 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.
@@ -16,8 +16,6 @@
 
 package org.springframework.amqp.rabbit.test.context;
 
-import static org.assertj.core.api.Assertions.assertThat;
-
 import org.junit.jupiter.api.Test;
 
 import org.springframework.amqp.rabbit.config.AbstractRabbitListenerContainerFactory;
@@ -32,6 +30,8 @@
 import org.springframework.test.annotation.DirtiesContext;
 import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 /**
  * @author Gary Russell
  * @since 2.3
diff --git a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerCaptureTest.java b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerCaptureTest.java
index 2be262a488..e01b6a0db2 100644
--- a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerCaptureTest.java
+++ b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerCaptureTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016-2022 the original author or authors.
+ * Copyright 2016-2025 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.
@@ -16,8 +16,6 @@
 
 package org.springframework.amqp.rabbit.test.examples;
 
-import static org.assertj.core.api.Assertions.assertThat;
-
 import java.util.concurrent.TimeUnit;
 
 import org.junit.jupiter.api.Test;
@@ -46,6 +44,8 @@
 import org.springframework.test.context.junit.jupiter.SpringExtension;
 import org.springframework.test.context.support.AnnotationConfigContextLoader;
 
+import static org.assertj.core.api.Assertions.assertThat;
+
 /**
  * @author Gary Russell
  * @author Artem Bilan
diff --git a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerSpyAndCaptureTest.java b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerSpyAndCaptureTest.java
index 03349f9e23..0cc500f3a2 100644
--- a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerSpyAndCaptureTest.java
+++ b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerSpyAndCaptureTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016-2020 the original author or authors.
+ * Copyright 2016-2025 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.
@@ -16,11 +16,6 @@
 
 package org.springframework.amqp.rabbit.test.examples;
 
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.BDDMockito.willAnswer;
-import static org.mockito.Mockito.verify;
-
 import java.util.Collection;
 import java.util.concurrent.TimeUnit;
 
@@ -47,6 +42,11 @@
 import org.springframework.test.annotation.DirtiesContext;
 import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
 
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.BDDMockito.willAnswer;
+import static org.mockito.Mockito.verify;
+
 /**
  * @author Gary Russell
  * @since 1.6
diff --git a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerSpyTest.java b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerSpyTest.java
index 45cddf63b5..6c35eda971 100644
--- a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerSpyTest.java
+++ b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/ExampleRabbitListenerSpyTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016-2020 the original author or authors.
+ * Copyright 2016-2025 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.
@@ -16,11 +16,6 @@
 
 package org.springframework.amqp.rabbit.test.examples;
 
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.BDDMockito.willAnswer;
-import static org.mockito.Mockito.verify;
-
 import org.junit.jupiter.api.Test;
 
 import org.springframework.amqp.core.AnonymousQueue;
@@ -44,6 +39,11 @@
 import org.springframework.test.annotation.DirtiesContext;
 import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
 
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.BDDMockito.willAnswer;
+import static org.mockito.Mockito.verify;
+
 /**
  * @author Gary Russell
  * @author Artem Bilan
diff --git a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/TestRabbitTemplateTests.java b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/TestRabbitTemplateTests.java
index 6ee389b821..39b6a48f4b 100644
--- a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/TestRabbitTemplateTests.java
+++ b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/examples/TestRabbitTemplateTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2017-2024 the original author or authors.
+ * Copyright 2017-2025 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.
@@ -16,16 +16,10 @@
 
 package org.springframework.amqp.rabbit.test.examples;
 
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyMap;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.BDDMockito.given;
-import static org.mockito.BDDMockito.willReturn;
-import static org.mockito.Mockito.mock;
-
 import java.io.IOException;
 
+import com.rabbitmq.client.AMQP;
+import com.rabbitmq.client.Channel;
 import org.junit.jupiter.api.Test;
 
 import org.springframework.amqp.rabbit.annotation.EnableRabbit;
@@ -42,8 +36,13 @@
 import org.springframework.test.annotation.DirtiesContext;
 import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
 
-import com.rabbitmq.client.AMQP;
-import com.rabbitmq.client.Channel;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyMap;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.willReturn;
+import static org.mockito.Mockito.mock;
 
 
 /**
diff --git a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/mockito/AnswerTests.java b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/mockito/AnswerTests.java
index f3e760be66..380b99c09d 100644
--- a/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/mockito/AnswerTests.java
+++ b/spring-rabbit-test/src/test/java/org/springframework/amqp/rabbit/test/mockito/AnswerTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2016-2020 the original author or authors.
+ * Copyright 2016-2025 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.
@@ -16,16 +16,16 @@
 
 package org.springframework.amqp.rabbit.test.mockito;
 
+import java.util.Collection;
+
+import org.junit.jupiter.api.Test;
+
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.BDDMockito.willAnswer;
 import static org.mockito.Mockito.spy;
 
-import java.util.Collection;
-
-import org.junit.jupiter.api.Test;
-
 /**
  * @author Gary Russell
  * @since 1.6
diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java
index 4a48611742..0389febb5c 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022-2024 the original author or authors.
+ * Copyright 2022-2025 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.
@@ -25,6 +25,7 @@
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 
+import com.rabbitmq.client.Channel;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
@@ -64,8 +65,6 @@
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 
-import com.rabbitmq.client.Channel;
-
 /**
  * Provides asynchronous send and receive operations returning a {@link CompletableFuture}
  * allowing the caller to obtain the reply later, using {@code get()} or a callback.
diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java
index b7ecb1a80d..a901c01100 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -35,6 +35,15 @@
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 
+import com.rabbitmq.client.Address;
+import com.rabbitmq.client.AddressResolver;
+import com.rabbitmq.client.BlockedListener;
+import com.rabbitmq.client.Method;
+import com.rabbitmq.client.Recoverable;
+import com.rabbitmq.client.RecoveryListener;
+import com.rabbitmq.client.ShutdownListener;
+import com.rabbitmq.client.ShutdownSignalException;
+import com.rabbitmq.client.impl.recovery.AutorecoveringConnection;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
@@ -55,16 +64,6 @@
 import org.springframework.util.StringUtils;
 import org.springframework.util.backoff.BackOff;
 
-import com.rabbitmq.client.Address;
-import com.rabbitmq.client.AddressResolver;
-import com.rabbitmq.client.BlockedListener;
-import com.rabbitmq.client.Method;
-import com.rabbitmq.client.Recoverable;
-import com.rabbitmq.client.RecoveryListener;
-import com.rabbitmq.client.ShutdownListener;
-import com.rabbitmq.client.ShutdownSignalException;
-import com.rabbitmq.client.impl.recovery.AutorecoveringConnection;
-
 /**
  * @author Dave Syer
  * @author Gary Russell
diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java
index 302b18a5cd..a160896b8c 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -49,6 +49,14 @@
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 
+import com.rabbitmq.client.Address;
+import com.rabbitmq.client.AlreadyClosedException;
+import com.rabbitmq.client.BlockedListener;
+import com.rabbitmq.client.Channel;
+import com.rabbitmq.client.ShutdownListener;
+import com.rabbitmq.client.ShutdownSignalException;
+import com.rabbitmq.client.impl.recovery.AutorecoveringChannel;
+
 import org.springframework.amqp.AmqpApplicationContextClosedException;
 import org.springframework.amqp.AmqpException;
 import org.springframework.amqp.AmqpTimeoutException;
@@ -63,14 +71,6 @@
 import org.springframework.util.ObjectUtils;
 import org.springframework.util.StringUtils;
 
-import com.rabbitmq.client.Address;
-import com.rabbitmq.client.AlreadyClosedException;
-import com.rabbitmq.client.BlockedListener;
-import com.rabbitmq.client.Channel;
-import com.rabbitmq.client.ShutdownListener;
-import com.rabbitmq.client.ShutdownSignalException;
-import com.rabbitmq.client.impl.recovery.AutorecoveringChannel;
-
 /**
  * A {@link ConnectionFactory} implementation that (when the cache mode is {@link CacheMode#CHANNEL} (default)
  * returns the same Connection from all {@link #createConnection()}
diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ChannelProxy.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ChannelProxy.java
index e0afe0fa28..d72d096344 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ChannelProxy.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ChannelProxy.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -16,10 +16,10 @@
 
 package org.springframework.amqp.rabbit.connection;
 
-import org.springframework.aop.RawTargetAccess;
-
 import com.rabbitmq.client.Channel;
 
+import org.springframework.aop.RawTargetAccess;
+
 /**
  * Subinterface of {@link com.rabbitmq.client.Channel} to be implemented by
  * Channel proxies.  Allows access to the underlying target Channel
diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ClosingRecoveryListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ClosingRecoveryListener.java
index e9a828b7fc..b12467bdfd 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ClosingRecoveryListener.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ClosingRecoveryListener.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-2022 the original author or authors.
+ * Copyright 2018-2025 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.
@@ -21,13 +21,12 @@
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.TimeoutException;
 
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-
 import com.rabbitmq.client.Channel;
 import com.rabbitmq.client.Recoverable;
 import com.rabbitmq.client.RecoveryListener;
 import com.rabbitmq.client.impl.recovery.AutorecoveringChannel;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
 
 /**
  * A {@link RecoveryListener} that closes the recovered channel, to avoid
diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/Connection.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/Connection.java
index 04ff8c38ec..ca704414e3 100755
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/Connection.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/Connection.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -16,12 +16,12 @@
 
 package org.springframework.amqp.rabbit.connection;
 
-import org.springframework.amqp.AmqpException;
-import org.springframework.lang.Nullable;
-
 import com.rabbitmq.client.BlockedListener;
 import com.rabbitmq.client.Channel;
 
+import org.springframework.amqp.AmqpException;
+import org.springframework.lang.Nullable;
+
 /**
  * @author Dave Syer
  * @author Gary Russell
diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryUtils.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryUtils.java
index 3c8897821a..ca1b1f7201 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryUtils.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryUtils.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -19,6 +19,8 @@
 import java.io.IOException;
 import java.util.function.Consumer;
 
+import com.rabbitmq.client.Channel;
+
 import org.springframework.amqp.AmqpIOException;
 import org.springframework.lang.Nullable;
 import org.springframework.transaction.support.ResourceHolderSynchronization;
@@ -27,8 +29,6 @@
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 
-import com.rabbitmq.client.Channel;
-
 /**
  * Helper class for managing a Spring based Rabbit {@link org.springframework.amqp.rabbit.connection.ConnectionFactory},
  * in particular for obtaining transactional Rabbit resources for a given ConnectionFactory.
diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConsumerChannelRegistry.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConsumerChannelRegistry.java
index e72c338b04..4ae774016e 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConsumerChannelRegistry.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConsumerChannelRegistry.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -16,13 +16,12 @@
 
 package org.springframework.amqp.rabbit.connection;
 
+import com.rabbitmq.client.Channel;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
 import org.springframework.lang.Nullable;
 
-import com.rabbitmq.client.Channel;
-
 /**
  * Consumers register their primary channels with this class. This is used
  * to ensure that, when using transactions, the resource holder doesn't
diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java
index 321c8925ab..d2ee951747 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2024 the original author or authors.
+ * Copyright 2020-2025 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.
@@ -24,6 +24,9 @@
 import java.util.concurrent.locks.ReentrantLock;
 import java.util.function.BiConsumer;
 
+import com.rabbitmq.client.Channel;
+import com.rabbitmq.client.ConnectionFactory;
+import com.rabbitmq.client.ShutdownListener;
 import org.aopalliance.aop.Advice;
 import org.aopalliance.intercept.MethodInterceptor;
 import org.apache.commons.logging.Log;
@@ -42,10 +45,6 @@
 import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
-import com.rabbitmq.client.Channel;
-import com.rabbitmq.client.ConnectionFactory;
-import com.rabbitmq.client.ShutdownListener;
-
 /**
  * A very simple connection factory that caches channels using Apache Pool2
  * {@link GenericObjectPool}s (one for transactional and one for non-transactional
diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java
index 74fb85bc95..3bc95d412d 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -37,19 +37,6 @@
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-
-import org.springframework.amqp.core.Message;
-import org.springframework.amqp.core.MessageProperties;
-import org.springframework.amqp.core.ReturnedMessage;
-import org.springframework.amqp.rabbit.connection.CorrelationData.Confirm;
-import org.springframework.amqp.rabbit.support.DefaultMessagePropertiesConverter;
-import org.springframework.amqp.rabbit.support.MessagePropertiesConverter;
-import org.springframework.lang.Nullable;
-import org.springframework.util.Assert;
-import org.springframework.util.StringUtils;
-
 import com.rabbitmq.client.AMQP;
 import com.rabbitmq.client.AMQP.Basic.RecoverOk;
 import com.rabbitmq.client.AMQP.BasicProperties;
@@ -82,6 +69,18 @@
 import com.rabbitmq.client.ShutdownListener;
 import com.rabbitmq.client.ShutdownSignalException;
 import com.rabbitmq.client.impl.recovery.AutorecoveringChannel;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.amqp.core.Message;
+import org.springframework.amqp.core.MessageProperties;
+import org.springframework.amqp.core.ReturnedMessage;
+import org.springframework.amqp.rabbit.connection.CorrelationData.Confirm;
+import org.springframework.amqp.rabbit.support.DefaultMessagePropertiesConverter;
+import org.springframework.amqp.rabbit.support.MessagePropertiesConverter;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
 
 /**
  * Channel wrapper to allow a single listener able to handle
diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java
index 6c91c630f7..768b789e86 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -16,6 +16,8 @@
 
 package org.springframework.amqp.rabbit.connection;
 
+import com.rabbitmq.client.Channel;
+import io.micrometer.observation.ObservationRegistry;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
@@ -26,9 +28,6 @@
 import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
-import com.rabbitmq.client.Channel;
-import io.micrometer.observation.ObservationRegistry;
-
 /**
  * @author Mark Fisher
  * @author Dave Syer
diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitConnectionFactoryBean.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitConnectionFactoryBean.java
index 944402231e..50ead3fe44 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitConnectionFactoryBean.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitConnectionFactoryBean.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -40,15 +40,6 @@
 import javax.net.ssl.TrustManager;
 import javax.net.ssl.TrustManagerFactory;
 
-import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator;
-import org.springframework.beans.factory.config.AbstractFactoryBean;
-import org.springframework.core.io.Resource;
-import org.springframework.core.io.ResourceLoader;
-import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
-import org.springframework.lang.Nullable;
-import org.springframework.util.Assert;
-import org.springframework.util.StringUtils;
-
 import com.rabbitmq.client.ConnectionFactory;
 import com.rabbitmq.client.ExceptionHandler;
 import com.rabbitmq.client.MetricsCollector;
@@ -58,6 +49,15 @@
 import com.rabbitmq.client.impl.CredentialsRefreshService;
 import com.rabbitmq.client.impl.nio.NioParams;
 
+import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator;
+import org.springframework.beans.factory.config.AbstractFactoryBean;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
 /**
  * Factory bean to create a RabbitMQ ConnectionFactory, delegating most setter methods and
  * optionally enabling SSL, with or without certificate validation. When
diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitResourceHolder.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitResourceHolder.java
index 80fd02e75a..925b3bf39f 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitResourceHolder.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitResourceHolder.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -22,6 +22,7 @@
 import java.util.List;
 import java.util.Map;
 
+import com.rabbitmq.client.Channel;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
@@ -32,8 +33,6 @@
 import org.springframework.util.Assert;
 import org.springframework.util.LinkedMultiValueMap;
 import org.springframework.util.MultiValueMap;
-
-import com.rabbitmq.client.Channel;
 /**
  * Rabbit resource holder, wrapping a RabbitMQ Connection and Channel.
  * RabbitTransactionManager binds instances of this
diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java
index e135d365a2..9a1fec7707 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -19,14 +19,6 @@
 import java.io.IOException;
 import java.util.Collection;
 
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-
-import org.springframework.amqp.AmqpIOException;
-import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator;
-import org.springframework.lang.Nullable;
-import org.springframework.util.Assert;
-
 import com.rabbitmq.client.AMQP;
 import com.rabbitmq.client.AlreadyClosedException;
 import com.rabbitmq.client.Channel;
@@ -37,6 +29,13 @@
 import com.rabbitmq.client.ShutdownSignalException;
 import com.rabbitmq.client.impl.CRDemoMechanism;
 import com.rabbitmq.client.impl.recovery.AutorecoveringChannel;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.amqp.AmqpIOException;
+import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
 
 /**
  * @author Mark Fisher
diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleConnection.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleConnection.java
index 9815f2c8cf..71515a26ad 100755
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleConnection.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleConnection.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -21,18 +21,18 @@
 
 import javax.annotation.Nullable;
 
-import org.springframework.amqp.AmqpResourceNotAvailableException;
-import org.springframework.amqp.AmqpTimeoutException;
-import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator;
-import org.springframework.util.ObjectUtils;
-import org.springframework.util.backoff.BackOffExecution;
-
 import com.rabbitmq.client.AlreadyClosedException;
 import com.rabbitmq.client.BlockedListener;
 import com.rabbitmq.client.Channel;
 import com.rabbitmq.client.impl.NetworkConnection;
 import com.rabbitmq.client.impl.recovery.AutorecoveringConnection;
 
+import org.springframework.amqp.AmqpResourceNotAvailableException;
+import org.springframework.amqp.AmqpTimeoutException;
+import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.backoff.BackOffExecution;
+
 /**
  * Simply a Connection.
  *
diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java
index 8097eed059..1c2702df5c 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2024 the original author or authors.
+ * Copyright 2020-2025 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.
@@ -25,6 +25,9 @@
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 
+import com.rabbitmq.client.Channel;
+import com.rabbitmq.client.ConnectionFactory;
+import com.rabbitmq.client.ShutdownListener;
 import org.aopalliance.aop.Advice;
 import org.aopalliance.intercept.MethodInterceptor;
 
@@ -36,10 +39,6 @@
 import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 
-import com.rabbitmq.client.Channel;
-import com.rabbitmq.client.ConnectionFactory;
-import com.rabbitmq.client.ShutdownListener;
-
 /**
  * A very simple connection factory that caches a channel per thread. Users are
  * responsible for releasing the thread's channel by calling
diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/ChannelCallback.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/ChannelCallback.java
index 4702175c57..0b24fcc3a8 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/ChannelCallback.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/ChannelCallback.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -16,10 +16,10 @@
 
 package org.springframework.amqp.rabbit.core;
 
-import org.springframework.lang.Nullable;
-
 import com.rabbitmq.client.Channel;
 
+import org.springframework.lang.Nullable;
+
 /**
  * Basic callback for use in RabbitTemplate.
  * @param  the type the callback returns.
diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java
index b85df8fece..f94f6bf573 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -34,6 +34,9 @@
 import java.util.concurrent.locks.ReentrantLock;
 import java.util.stream.Collectors;
 
+import com.rabbitmq.client.AMQP.Queue.DeclareOk;
+import com.rabbitmq.client.AMQP.Queue.PurgeOk;
+import com.rabbitmq.client.Channel;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
@@ -67,10 +70,6 @@
 import org.springframework.util.Assert;
 import org.springframework.util.StringUtils;
 
-import com.rabbitmq.client.AMQP.Queue.DeclareOk;
-import com.rabbitmq.client.AMQP.Queue.PurgeOk;
-import com.rabbitmq.client.Channel;
-
 /**
  * RabbitMQ implementation of portable AMQP administrative operations for AMQP >= 0.9.1.
  *
diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java
index 77315c4020..de3af0c8a6 100644
--- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java
+++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -39,6 +39,20 @@
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 
+import com.rabbitmq.client.AMQP;
+import com.rabbitmq.client.AMQP.BasicProperties;
+import com.rabbitmq.client.AMQP.Queue.DeclareOk;
+import com.rabbitmq.client.Channel;
+import com.rabbitmq.client.ConfirmListener;
+import com.rabbitmq.client.DefaultConsumer;
+import com.rabbitmq.client.Envelope;
+import com.rabbitmq.client.GetResponse;
+import com.rabbitmq.client.Return;
+import com.rabbitmq.client.ShutdownListener;
+import com.rabbitmq.client.ShutdownSignalException;
+import io.micrometer.observation.Observation;
+import io.micrometer.observation.ObservationRegistry;
+
 import org.springframework.amqp.AmqpConnectException;
 import org.springframework.amqp.AmqpException;
 import org.springframework.amqp.AmqpIOException;
@@ -106,20 +120,6 @@
 import org.springframework.util.ErrorHandler;
 import org.springframework.util.StringUtils;
 
-import com.rabbitmq.client.AMQP;
-import com.rabbitmq.client.AMQP.BasicProperties;
-import com.rabbitmq.client.AMQP.Queue.DeclareOk;
-import com.rabbitmq.client.Channel;
-import com.rabbitmq.client.ConfirmListener;
-import com.rabbitmq.client.DefaultConsumer;
-import com.rabbitmq.client.Envelope;
-import com.rabbitmq.client.GetResponse;
-import com.rabbitmq.client.Return;
-import com.rabbitmq.client.ShutdownListener;
-import com.rabbitmq.client.ShutdownSignalException;
-import io.micrometer.observation.Observation;
-import io.micrometer.observation.ObservationRegistry;
-
 /**
  * 

* Helper class that simplifies synchronous RabbitMQ access (sending and receiving messages). diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index 80df1dab3d..c4c4335e71 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -33,6 +33,9 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; +import com.rabbitmq.client.Channel; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; import org.aopalliance.aop.Advice; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -92,10 +95,6 @@ import org.springframework.util.backoff.BackOff; import org.springframework.util.backoff.FixedBackOff; -import com.rabbitmq.client.Channel; -import io.micrometer.observation.Observation; -import io.micrometer.observation.ObservationRegistry; - /** * @author Mark Pollack * @author Mark Fisher diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java index dd1d6647bc..9e9fa598a7 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -39,6 +39,12 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.DefaultConsumer; +import com.rabbitmq.client.Envelope; +import com.rabbitmq.client.ShutdownSignalException; +import com.rabbitmq.utility.Utility; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -71,13 +77,6 @@ import org.springframework.util.ObjectUtils; import org.springframework.util.backoff.BackOffExecution; -import com.rabbitmq.client.AMQP; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.DefaultConsumer; -import com.rabbitmq.client.Envelope; -import com.rabbitmq.client.ShutdownSignalException; -import com.rabbitmq.utility.Utility; - /** * Specialized consumer encapsulating knowledge of the broker * connections and having its own lifecycle (start and stop). diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java index 44ffc36f77..c7db111bdd 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 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. @@ -41,6 +41,11 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.DefaultConsumer; +import com.rabbitmq.client.Envelope; +import com.rabbitmq.client.ShutdownSignalException; import org.apache.commons.logging.Log; import org.springframework.amqp.AmqpApplicationContextClosedException; @@ -82,12 +87,6 @@ import org.springframework.util.StringUtils; import org.springframework.util.backoff.BackOffExecution; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.DefaultConsumer; -import com.rabbitmq.client.Envelope; -import com.rabbitmq.client.ShutdownSignalException; - /** * The {@code SimpleMessageListenerContainer} is not so simple. Recent changes to the * rabbitmq java client has facilitated a much simpler listener container that invokes the diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainer.java index d4dc2278b7..15a95465c4 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 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. @@ -20,6 +20,8 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; +import com.rabbitmq.client.Channel; + import org.springframework.amqp.AmqpTimeoutException; import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.Address; @@ -29,8 +31,6 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import com.rabbitmq.client.Channel; - /** * Listener container for Direct ReplyTo only listens to the pseudo queue * {@link Address#AMQ_RABBITMQ_REPLY_TO}. Consumers are added on-demand and diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MicrometerHolder.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MicrometerHolder.java index 5cd536329f..422ffc67bf 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MicrometerHolder.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MicrometerHolder.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 the original author or authors. + * Copyright 2022-2025 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. @@ -20,15 +20,15 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import org.springframework.beans.factory.NoUniqueBeanDefinitionException; -import org.springframework.context.ApplicationContext; -import org.springframework.lang.Nullable; - import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.Timer.Builder; import io.micrometer.core.instrument.Timer.Sample; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.lang.Nullable; + /** * Abstraction to avoid hard reference to Micrometer. * diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java index f4f41b4de6..06471602b3 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -30,6 +30,10 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.PossibleAuthenticationFailureException; +import com.rabbitmq.client.ShutdownSignalException; + import org.springframework.amqp.AmqpAuthenticationException; import org.springframework.amqp.AmqpConnectException; import org.springframework.amqp.AmqpException; @@ -67,10 +71,6 @@ import org.springframework.util.Assert; import org.springframework.util.backoff.BackOffExecution; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.PossibleAuthenticationFailureException; -import com.rabbitmq.client.ShutdownSignalException; - /** * @author Mark Pollack * @author Mark Fisher diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java index 1eab40f07a..aa3e426cd5 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 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. @@ -23,6 +23,7 @@ import java.util.Arrays; import java.util.concurrent.CompletableFuture; +import com.rabbitmq.client.Channel; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -54,8 +55,6 @@ import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import com.rabbitmq.client.Channel; - /** * An abstract {@link org.springframework.amqp.core.MessageListener} adapter providing the * necessary infrastructure to extract the payload of a {@link Message}. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapter.java index fcedab367f..b3eb8bf968 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 the original author or authors. + * Copyright 2019-2025 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. @@ -22,6 +22,8 @@ import java.util.List; import java.util.concurrent.CompletableFuture; +import com.rabbitmq.client.Channel; + import org.springframework.amqp.rabbit.batch.BatchingStrategy; import org.springframework.amqp.rabbit.batch.SimpleBatchingStrategy; import org.springframework.amqp.rabbit.listener.api.ChannelAwareBatchMessageListener; @@ -34,8 +36,6 @@ import org.springframework.messaging.support.GenericMessage; import org.springframework.messaging.support.MessageBuilder; -import com.rabbitmq.client.Channel; - /** * A listener adapter for batch listeners. * diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/ContinuationHandlerMethodArgumentResolver.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/ContinuationHandlerMethodArgumentResolver.java index a9f2dcd87f..f35a6dd874 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/ContinuationHandlerMethodArgumentResolver.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/ContinuationHandlerMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2023-2025 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. @@ -16,12 +16,12 @@ package org.springframework.amqp.rabbit.listener.adapter; +import reactor.core.publisher.Mono; + import org.springframework.core.MethodParameter; import org.springframework.messaging.Message; import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; -import reactor.core.publisher.Mono; - /** * No-op resolver for method arguments of type {@link kotlin.coroutines.Continuation}. *

diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapter.java index b101a33954..26222b4529 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -22,6 +22,8 @@ import java.util.HashMap; import java.util.Map; +import com.rabbitmq.client.Channel; + import org.springframework.amqp.AmqpIOException; import org.springframework.amqp.AmqpIllegalStateException; import org.springframework.amqp.core.Message; @@ -35,8 +37,6 @@ import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; -import com.rabbitmq.client.Channel; - /** * Message listener adapter that delegates the handling of messages to target listener methods via reflection, with * flexible message type conversion. Allows listener methods to operate on message content types, completely independent diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java index 88313d0bd2..ba7b52821c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -24,6 +24,8 @@ import java.util.List; import java.util.Optional; +import com.rabbitmq.client.Channel; + import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.rabbit.listener.api.RabbitListenerErrorHandler; import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; @@ -43,8 +45,6 @@ import org.springframework.util.Assert; import org.springframework.util.TypeUtils; -import com.rabbitmq.client.Channel; - /** * A {@link org.springframework.amqp.core.MessageListener MessageListener} * adapter that invokes a configurable {@link HandlerAdapter}. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/ChannelAwareBatchMessageListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/ChannelAwareBatchMessageListener.java index f80cad7523..990593943e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/ChannelAwareBatchMessageListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/ChannelAwareBatchMessageListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-2025 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. @@ -18,10 +18,10 @@ import java.util.List; -import org.springframework.amqp.core.Message; - import com.rabbitmq.client.Channel; +import org.springframework.amqp.core.Message; + /** * Used to receive a batch of messages if the container supports it. * diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/ChannelAwareMessageListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/ChannelAwareMessageListener.java index 3ab039d2e2..a5ad115df5 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/ChannelAwareMessageListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/ChannelAwareMessageListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 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. @@ -18,12 +18,12 @@ import java.util.List; +import com.rabbitmq.client.Channel; + import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageListener; import org.springframework.lang.Nullable; -import com.rabbitmq.client.Channel; - /** * A message listener that is aware of the Channel on which the message was received. * diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/RabbitListenerErrorHandler.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/RabbitListenerErrorHandler.java index 53eb43c6d2..4d9e800f75 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/RabbitListenerErrorHandler.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/RabbitListenerErrorHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 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. @@ -16,11 +16,11 @@ package org.springframework.amqp.rabbit.listener.api; +import com.rabbitmq.client.Channel; + import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; import org.springframework.lang.Nullable; - -import com.rabbitmq.client.Channel; /** * An error handler which is called when a {code @RabbitListener} method * throws an exception. This is invoked higher up the stack than the diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j2/AmqpAppender.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j2/AmqpAppender.java index d76c38a990..4f2a19fccc 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j2/AmqpAppender.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j2/AmqpAppender.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 the original author or authors. + * Copyright 2016-2025 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. @@ -36,6 +36,7 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import com.rabbitmq.client.ConnectionFactory; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.core.Filter; import org.apache.logging.log4j.core.Layout; @@ -85,8 +86,6 @@ import org.springframework.util.Assert; import org.springframework.util.StringUtils; -import com.rabbitmq.client.ConnectionFactory; - /** * A Log4j 2 appender that publishes logging events to an AMQP Exchange. * diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/AmqpAppender.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/AmqpAppender.java index ba5a6e25f9..8f662afcd6 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/AmqpAppender.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/AmqpAppender.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2025 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. @@ -32,6 +32,16 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.PatternLayout; +import ch.qos.logback.classic.pattern.TargetLengthBasedClassNameAbbreviator; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.classic.spi.LoggingEvent; +import ch.qos.logback.core.AppenderBase; +import ch.qos.logback.core.Layout; +import ch.qos.logback.core.encoder.Encoder; +import com.rabbitmq.client.ConnectionFactory; + import org.springframework.amqp.AmqpApplicationContextClosedException; import org.springframework.amqp.AmqpException; import org.springframework.amqp.core.DirectExchange; @@ -59,16 +69,6 @@ import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.util.StringUtils; -import ch.qos.logback.classic.Level; -import ch.qos.logback.classic.PatternLayout; -import ch.qos.logback.classic.pattern.TargetLengthBasedClassNameAbbreviator; -import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.classic.spi.LoggingEvent; -import ch.qos.logback.core.AppenderBase; -import ch.qos.logback.core.Layout; -import ch.qos.logback.core.encoder.Encoder; -import com.rabbitmq.client.ConnectionFactory; - /** * A Logback appender that publishes logging events to an AMQP Exchange. *

diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java index 44ccbdfe0e..2a9bf33f71 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -24,16 +24,16 @@ import java.util.List; import java.util.Map; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Envelope; +import com.rabbitmq.client.LongString; + import org.springframework.amqp.core.MessageDeliveryMode; import org.springframework.amqp.core.MessageProperties; import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.Envelope; -import com.rabbitmq.client.LongString; - /** * Default implementation of the {@link MessagePropertiesConverter} strategy. * diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/MessagePropertiesConverter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/MessagePropertiesConverter.java index 8c91016f05..4f18deedb5 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/MessagePropertiesConverter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/MessagePropertiesConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -16,12 +16,12 @@ package org.springframework.amqp.rabbit.support; -import org.springframework.amqp.core.MessageProperties; -import org.springframework.lang.Nullable; - import com.rabbitmq.client.AMQP.BasicProperties; import com.rabbitmq.client.Envelope; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.lang.Nullable; + /** * Strategy interface for converting between Spring AMQP {@link MessageProperties} * and RabbitMQ BasicProperties. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/RabbitExceptionTranslator.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/RabbitExceptionTranslator.java index ce1e460eca..c4b3825220 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/RabbitExceptionTranslator.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/RabbitExceptionTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -21,6 +21,10 @@ import java.net.ConnectException; import java.util.concurrent.TimeoutException; +import com.rabbitmq.client.ConsumerCancelledException; +import com.rabbitmq.client.PossibleAuthenticationFailureException; +import com.rabbitmq.client.ShutdownSignalException; + import org.springframework.amqp.AmqpAuthenticationException; import org.springframework.amqp.AmqpConnectException; import org.springframework.amqp.AmqpException; @@ -30,10 +34,6 @@ import org.springframework.amqp.UncategorizedAmqpException; import org.springframework.util.Assert; -import com.rabbitmq.client.ConsumerCancelledException; -import com.rabbitmq.client.PossibleAuthenticationFailureException; -import com.rabbitmq.client.ShutdownSignalException; - /** * Translates Rabbit Exceptions to the {@link AmqpException} class * hierarchy. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java index 3caebc40a6..ee29b9b848 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2025 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. @@ -16,10 +16,10 @@ package org.springframework.amqp.rabbit.support.micrometer; -import org.springframework.amqp.core.Message; - import io.micrometer.observation.transport.ReceiverContext; +import org.springframework.amqp.core.Message; + /** * {@link ReceiverContext} for {@link Message}s. * diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageSenderContext.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageSenderContext.java index b7994ad645..c6521abbb5 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageSenderContext.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageSenderContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 the original author or authors. + * Copyright 2022-2025 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. @@ -16,10 +16,10 @@ package org.springframework.amqp.rabbit.support.micrometer; -import org.springframework.amqp.core.Message; - import io.micrometer.observation.transport.SenderContext; +import org.springframework.amqp.core.Message; + /** * {@link SenderContext} for {@link Message}s. * diff --git a/spring-rabbit/src/main/resources/org/springframework/amqp/rabbit/config/spring-rabbit.xsd b/spring-rabbit/src/main/resources/org/springframework/amqp/rabbit/config/spring-rabbit.xsd index 3415435c15..2414c7779f 100644 --- a/spring-rabbit/src/main/resources/org/springframework/amqp/rabbit/config/spring-rabbit.xsd +++ b/spring-rabbit/src/main/resources/org/springframework/amqp/rabbit/config/spring-rabbit.xsd @@ -1,8 +1,9 @@ + xmlns:tool="http://www.springframework.org/schema/tool" + xmlns:beans="http://www.springframework.org/schema/beans" + targetNamespace="http://www.springframework.org/schema/rabbit" + elementFormDefault="qualified" attributeFormDefault="unqualified"> diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java index 153445d1b7..a3db5738b4 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/AsyncRabbitTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 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. @@ -16,11 +16,6 @@ package org.springframework.amqp.rabbit; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.awaitility.Awaitility.await; -import static org.mockito.Mockito.mock; - import java.time.Duration; import java.util.Map; import java.util.UUID; @@ -69,6 +64,11 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.awaitility.Awaitility.await; +import static org.mockito.Mockito.mock; + /** * @author Gary Russell * @author Artem Bilan @@ -101,7 +101,7 @@ static void setup() { } @Test - public void testConvert1Arg() { + public void testConvert1Arg() { final AtomicBoolean mppCalled = new AtomicBoolean(); CompletableFuture future = this.asyncTemplate.convertSendAndReceive("foo", m -> { mppCalled.set(true); @@ -402,6 +402,7 @@ public void testStopCancelled() throws Exception { @DirtiesContext public void testConversionException() { this.asyncTemplate.getRabbitTemplate().setMessageConverter(new SimpleMessageConverter() { + @Override public Object fromMessage(Message message) throws MessageConversionException { throw new MessageConversionException("Failed to convert message"); @@ -436,7 +437,7 @@ void ctorCoverage() { .isEqualTo("rq"); assertThat(template).extracting("container") .extracting("queueNames") - .isEqualTo(new String[] { "rq" }); + .isEqualTo(new String[] {"rq"}); template = new AsyncRabbitTemplate(mock(ConnectionFactory.class), "ex", "rk", "rq", "ra"); assertThat(template).extracting(AsyncRabbitTemplate::getRabbitTemplate) .extracting("exchange") @@ -449,7 +450,7 @@ void ctorCoverage() { .isEqualTo("ra"); assertThat(template).extracting("container") .extracting("queueNames") - .isEqualTo(new String[] { "rq" }); + .isEqualTo(new String[] {"rq"}); template = new AsyncRabbitTemplate(mock(RabbitTemplate.class), mock(AbstractMessageListenerContainer.class), "rq"); assertThat(template) @@ -510,7 +511,6 @@ public static class TheCallback implements BiConsumer { private volatile Throwable ex; - @Override public void accept(String result, Throwable ex) { this.result = result; diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AbstractRabbitAnnotationDrivenTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AbstractRabbitAnnotationDrivenTests.java index bdfb6abbca..88f40c7584 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AbstractRabbitAnnotationDrivenTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AbstractRabbitAnnotationDrivenTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 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. @@ -16,13 +16,10 @@ package org.springframework.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; -import static org.mockito.Mockito.mock; - import java.util.Collection; import java.util.Map; +import com.rabbitmq.client.Channel; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Message; @@ -42,7 +39,9 @@ import org.springframework.validation.Validator; import org.springframework.validation.annotation.Validated; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.Mockito.mock; /** * @@ -283,6 +282,7 @@ public void defaultHandle(String msg) { @RabbitListener(containerFactory = "simpleFactory", queues = "myQueue") public void simpleHandle(String msg) { } + } @Component @@ -293,6 +293,7 @@ static class FullBean { public void fullHandle(String msg) { } + } @Component @@ -305,6 +306,7 @@ static class FullConfigurableBean { public void fullHandle(String msg) { } + } @Component @@ -313,6 +315,7 @@ static class CustomBean { @RabbitListener(id = "listenerId", containerFactory = "customFactory", queues = "myQueue") public void customHandle(String msg) { } + } static class DefaultBean { @@ -320,6 +323,7 @@ static class DefaultBean { @RabbitListener(queues = "myQueue") public void handleIt(String msg) { } + } @Component @@ -328,6 +332,7 @@ static class ValidationBean { @RabbitListener(containerFactory = "defaultFactory", queues = "myQueue") public void defaultHandle(@Validated String msg) { } + } @Component @@ -365,6 +370,7 @@ public void validate(Object target, Errors errors) { errors.reject("TEST: expected invalid value"); } } + } } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AnnotationDrivenNamespaceTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AnnotationDrivenNamespaceTests.java index a180515af6..617bc5eda9 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AnnotationDrivenNamespaceTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AnnotationDrivenNamespaceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -16,9 +16,6 @@ package org.springframework.amqp.rabbit.annotation; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; - import org.junit.jupiter.api.Test; import org.springframework.amqp.core.MessageListener; @@ -30,6 +27,8 @@ import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.messaging.handler.annotation.support.MethodArgumentNotValidException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + /** * @author Stephane Nicoll * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AsyncListenerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AsyncListenerTests.java index 9ef3250623..cd4a3a6ded 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AsyncListenerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/AsyncListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * Copyright 2018-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -30,6 +28,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; import org.springframework.amqp.AmqpRejectAndDontRequeueException; import org.springframework.amqp.ImmediateRequeueAmqpException; @@ -55,7 +54,7 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import reactor.core.publisher.Mono; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ComplexTypeJsonIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ComplexTypeJsonIntegrationTests.java index 2b9d34276f..18546a16f1 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ComplexTypeJsonIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ComplexTypeJsonIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 the original author or authors. + * Copyright 2016-2025 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. @@ -16,9 +16,6 @@ package org.springframework.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; - import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; @@ -42,6 +39,9 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + /** * @author Gary Russell * @since 2.0 diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ConsumerBatchingTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ConsumerBatchingTests.java index 617fef6c1c..48373d1b28 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ConsumerBatchingTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ConsumerBatchingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2020 the original author or authors. + * Copyright 2019-2025 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. @@ -16,9 +16,6 @@ package org.springframework.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; - import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; @@ -27,6 +24,10 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import com.rabbitmq.client.Channel; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.jupiter.api.Test; import org.springframework.amqp.AmqpRejectAndDontRequeueException; @@ -48,10 +49,8 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import com.rabbitmq.client.Channel; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Timer; -import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ContentTypeDelegatingMessageConverterIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ContentTypeDelegatingMessageConverterIntegrationTests.java index 3ac031bcd0..76438d1267 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ContentTypeDelegatingMessageConverterIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/ContentTypeDelegatingMessageConverterIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 the original author or authors. + * Copyright 2020-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -48,6 +46,8 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.util.MimeType; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * @since 2.5 diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchIntegrationTests.java index 8121e395bb..02c80cb434 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 the original author or authors. + * Copyright 2019-2025 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. @@ -16,8 +16,6 @@ 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; @@ -47,6 +45,8 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * @since 2.2 diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchJsonIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchJsonIntegrationTests.java index 3c720291bc..4f5f92b780 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchJsonIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitBatchJsonIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2020 the original author or authors. + * Copyright 2019-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; - import java.io.UnsupportedEncodingException; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -44,6 +42,8 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * @author Kai Stapel diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitCglibProxyTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitCglibProxyTests.java index c0ab010464..fecca9e67e 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitCglibProxyTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitCglibProxyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 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. @@ -16,9 +16,6 @@ package org.springframework.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - import java.io.Serializable; import org.junit.jupiter.api.Test; @@ -40,6 +37,9 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Transactional; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + /** * @author Artem Bilan * @since 1.5.5 diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIdleContainerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIdleContainerTests.java index e01a2bfbe8..4f23fc61b1 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIdleContainerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIdleContainerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -45,6 +43,8 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * @since 1.6 diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java index 5c91161892..5478f9bf5a 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 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. @@ -16,16 +16,6 @@ package org.springframework.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; -import static org.awaitility.Awaitility.await; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - import java.io.IOException; import java.io.Serializable; import java.lang.annotation.ElementType; @@ -47,6 +37,11 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.Channel; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import jakarta.validation.Valid; import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; @@ -148,11 +143,15 @@ import org.springframework.validation.Validator; import org.springframework.validation.annotation.Validated; -import com.rabbitmq.client.Channel; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Timer; -import io.micrometer.core.instrument.simple.SimpleMeterRegistry; -import jakarta.validation.Valid; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; /** * diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitReturnTypesTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitReturnTypesTests.java index a99fb81402..8783d66379 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitReturnTypesTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitReturnTypesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 the original author or authors. + * Copyright 2019-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.ArrayList; import java.util.List; @@ -44,6 +42,8 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * @since 2.2 diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitTests.java index b7816201b8..2f10a27494 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -16,15 +16,7 @@ package org.springframework.amqp.rabbit.annotation; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; - +import com.rabbitmq.client.Channel; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.MessageListener; @@ -54,7 +46,13 @@ import org.springframework.messaging.handler.annotation.support.MethodArgumentNotValidException; import org.springframework.stereotype.Component; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; /** * @author Stephane Nicoll diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/LazyContainerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/LazyContainerTests.java index d6be1c75bb..0e7a97159d 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/LazyContainerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/LazyContainerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; - import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Queue; @@ -36,6 +34,8 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * @since 2.1.5 diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MessageHandlerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MessageHandlerTests.java index bc918fbaa3..bd629702c8 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MessageHandlerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MessageHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-2025 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. @@ -16,15 +16,13 @@ package org.springframework.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - import java.lang.reflect.Method; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import com.rabbitmq.client.Channel; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.MessageProperties; @@ -39,7 +37,8 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.util.ReflectionUtils; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MockMultiRabbitTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MockMultiRabbitTests.java index 22956228d2..d0900ecd1b 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MockMultiRabbitTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MockMultiRabbitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 the original author or authors. + * Copyright 2020-2025 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. @@ -22,6 +22,7 @@ import java.util.function.BiFunction; import java.util.stream.Collectors; +import com.rabbitmq.client.Channel; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -53,8 +54,6 @@ import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; import org.springframework.stereotype.Component; -import com.rabbitmq.client.Channel; - /** * @author Wander Costa */ diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MultiRabbitBootstrapConfigurationTest.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MultiRabbitBootstrapConfigurationTests.java similarity index 96% rename from spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MultiRabbitBootstrapConfigurationTest.java rename to spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MultiRabbitBootstrapConfigurationTests.java index 39388710dc..dc495ead2e 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MultiRabbitBootstrapConfigurationTest.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/MultiRabbitBootstrapConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * Copyright 2014-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; - import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -28,7 +26,9 @@ import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.env.Environment; -class MultiRabbitBootstrapConfigurationTest { +import static org.assertj.core.api.Assertions.assertThat; + +class MultiRabbitBootstrapConfigurationTests { @Test @DisplayName("test if MultiRabbitBPP is registered when enabled") @@ -67,4 +67,5 @@ void testMultiRabbitBPPIsNotRegistered() throws Exception { Mockito.verify(registry, Mockito.never()).registerBeanDefinition(Mockito.anyString(), Mockito.any(RootBeanDefinition.class)); } + } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/OptionalPayloadTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/OptionalPayloadTests.java index 56790f4eab..b699875409 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/OptionalPayloadTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/OptionalPayloadTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 the original author or authors. + * Copyright 2022-2025 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. @@ -16,14 +16,14 @@ package org.springframework.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.springframework.amqp.AmqpException; @@ -43,8 +43,7 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessorTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessorTests.java index 6ef00c5932..72cea92b57 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessorTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.annotation; -import static org.assertj.core.api.Assertions.assertThat; - import java.lang.annotation.ElementType; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; @@ -59,6 +57,8 @@ import org.springframework.core.annotation.AliasFor; import org.springframework.stereotype.Component; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Stephane Nicoll * @author Juergen Hoeller diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/AdminParserTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/AdminParserTests.java index 48d07c4bd3..226ddfad93 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/AdminParserTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/AdminParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -16,9 +16,6 @@ package org.springframework.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.Test; @@ -33,6 +30,9 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.util.StringUtils; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + /** * * @author tomas.lukosius@opencredo.com diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizerTests.java index 3162721c76..356a1d9909 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/CompositeContainerCustomizerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2025 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. @@ -16,15 +16,15 @@ package org.springframework.amqp.rabbit.config; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.listener.MessageListenerContainer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + /** * @author Gary Russell * @since 2.4.8 diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ConnectionFactoryParserTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ConnectionFactoryParserTests.java index 20256ff2f8..e0ef32f967 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ConnectionFactoryParserTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ConnectionFactoryParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 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. @@ -16,11 +16,11 @@ package org.springframework.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.List; import java.util.concurrent.ExecutorService; +import com.rabbitmq.client.Address; +import com.rabbitmq.client.ConnectionFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -35,8 +35,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; -import com.rabbitmq.client.Address; -import com.rabbitmq.client.ConnectionFactory; +import static org.assertj.core.api.Assertions.assertThat; /** * diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ExchangeParserIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ExchangeParserIntegrationTests.java index 652134a69e..113b0d251e 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ExchangeParserIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ExchangeParserIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; - import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -33,6 +31,8 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Dave Syer * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ExchangeParserTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ExchangeParserTests.java index db6dee3814..02da3d145b 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ExchangeParserTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ExchangeParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Map; import org.junit.jupiter.api.BeforeEach; @@ -32,6 +30,8 @@ import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.core.io.ClassPathResource; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Dave Syer * @author Mark Fisher diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBeanTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBeanTests.java index 3b9602616c..4fb1b54f2e 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBeanTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBeanTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2025 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. @@ -16,9 +16,6 @@ package org.springframework.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - import java.util.Map; import org.junit.jupiter.api.Test; @@ -28,6 +25,9 @@ import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; import org.springframework.amqp.utils.test.TestUtils; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + /** * @author Gary Russell * @since 2.4.6 diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerParserTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerParserTests.java index 28b5a9cf10..727b773e8f 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerParserTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2021 the original author or authors. + * Copyright 2010-2025 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. @@ -16,9 +16,6 @@ package org.springframework.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collection; @@ -46,6 +43,9 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.test.util.ReflectionTestUtils; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + /** * @author Mark Fisher * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerPlaceholderParserTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerPlaceholderParserTests.java index ebbdd8a5ab..0d55a932f8 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerPlaceholderParserTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/ListenerContainerPlaceholderParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2021 the original author or authors. + * Copyright 2010-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Arrays; import java.util.concurrent.ExecutorService; import java.util.concurrent.ThreadPoolExecutor; @@ -39,6 +37,8 @@ import org.springframework.context.support.GenericXmlApplicationContext; import org.springframework.core.io.ClassPathResource; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Dave Syer * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/MismatchedQueueDeclarationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/MismatchedQueueDeclarationTests.java index 1c6fa21fc8..4e12d003a4 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/MismatchedQueueDeclarationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/MismatchedQueueDeclarationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -16,9 +16,7 @@ package org.springframework.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - +import com.rabbitmq.client.Channel; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; @@ -32,7 +30,8 @@ import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.core.env.StandardEnvironment; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/QueueArgumentsParserTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/QueueArgumentsParserTests.java index 3940e1fbee..13adfc1c5a 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/QueueArgumentsParserTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/QueueArgumentsParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Map; import org.junit.jupiter.api.Test; @@ -28,6 +26,8 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/QueueParserIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/QueueParserIntegrationTests.java index d00c887184..f4fd60665a 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/QueueParserIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/QueueParserIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 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. @@ -16,9 +16,6 @@ package org.springframework.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; - import java.time.Duration; import org.junit.jupiter.api.BeforeEach; @@ -34,6 +31,9 @@ import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.core.io.ClassPathResource; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + /** * @author Dave Syer * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/QueueParserTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/QueueParserTests.java index 076633dd83..411345a0c5 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/QueueParserTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/QueueParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -16,9 +16,6 @@ package org.springframework.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -31,6 +28,9 @@ import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.core.io.ClassPathResource; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + /** * @author Dave Syer * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerFactoryIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerFactoryIntegrationTests.java index 87fc06907b..576b0cd282 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerFactoryIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerFactoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -16,14 +16,12 @@ package org.springframework.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - import java.lang.reflect.Method; import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import com.rabbitmq.client.Channel; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -42,7 +40,8 @@ import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory; import org.springframework.util.ReflectionUtils; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * @author Stephane Nicoll diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerFactoryTests.java index 6b4242150e..780951c387 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 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. @@ -16,10 +16,6 @@ package org.springframework.amqp.rabbit.config; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - import java.util.List; import java.util.concurrent.Executor; @@ -44,6 +40,9 @@ import org.springframework.util.backoff.BackOff; import org.springframework.util.backoff.ExponentialBackOff; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + /** * @author Stephane Nicoll * @author Artem Bilan diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerTestFactory.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerTestFactory.java index 9e68785e27..1360ebd5e8 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerTestFactory.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitListenerContainerTestFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -28,6 +26,8 @@ import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory; import org.springframework.amqp.rabbit.listener.RabbitListenerEndpoint; +import static org.assertj.core.api.Assertions.assertThat; + /** * * @author Stephane Nicoll diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitNamespaceHandlerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitNamespaceHandlerTests.java index 534e47e7e4..e96c9a7ebe 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitNamespaceHandlerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RabbitNamespaceHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Map; import org.junit.jupiter.api.BeforeEach; @@ -35,6 +33,8 @@ import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.core.io.ClassPathResource; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Tomas Lukosius * @author Dave Syer diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RetryInterceptorBuilderSupportTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RetryInterceptorBuilderSupportTests.java index 49581837ea..e30a18e06d 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RetryInterceptorBuilderSupportTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/RetryInterceptorBuilderSupportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 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. @@ -16,10 +16,6 @@ package org.springframework.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - import java.util.Collections; import java.util.Map; import java.util.concurrent.CountDownLatch; @@ -47,6 +43,10 @@ import org.springframework.retry.policy.SimpleRetryPolicy; import org.springframework.retry.support.RetryTemplate; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + /** * @author Gary Russell * @author Artem Bilan diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerEndpointTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerEndpointTests.java index 738b4bcf74..8e1340c282 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerEndpointTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerEndpointTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -16,11 +16,6 @@ package org.springframework.amqp.rabbit.config; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.Mockito.mock; - import org.junit.jupiter.api.Test; import org.springframework.amqp.core.MessageListener; @@ -28,6 +23,10 @@ import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.Mockito.mock; + /** * @author Stephane Nicoll * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/TemplateParserTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/TemplateParserTests.java index 7ef29168e2..be8cef0699 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/TemplateParserTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/TemplateParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.config; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Collection; import org.junit.jupiter.api.BeforeEach; @@ -36,6 +34,8 @@ import org.springframework.retry.RecoveryCallback; import org.springframework.retry.support.RetryTemplate; +import static org.assertj.core.api.Assertions.assertThat; + /** * * @author Dave Syer diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactoryTests.java index 097ddf9632..04b470d2e7 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2024 the original author or authors. + * Copyright 2010-2025 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. @@ -16,28 +16,14 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willCallRealMethod; -import static org.mockito.BDDMockito.willReturn; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.ConnectionFactory; import org.apache.commons.logging.Log; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -50,8 +36,21 @@ import org.springframework.util.StopWatch; import org.springframework.util.backoff.FixedBackOff; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.ConnectionFactory; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willCallRealMethod; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; /** * @author Dave Syer diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachePropertiesTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachePropertiesTests.java index 16c501f047..93e31077fd 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachePropertiesTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachePropertiesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 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. @@ -16,11 +16,10 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Properties; import java.util.concurrent.atomic.AtomicInteger; +import com.rabbitmq.client.Channel; import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory.CacheMode; @@ -31,7 +30,7 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryIntegrationTests.java index 668237258f..3b21dfd2e3 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 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. @@ -16,16 +16,6 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assertions.fail; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; @@ -41,6 +31,8 @@ import javax.net.ServerSocketFactory; import javax.net.SocketFactory; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.DefaultConsumer; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.AfterEach; @@ -67,8 +59,15 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.event.ContextClosedEvent; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.DefaultConsumer; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; /** * @author Dave Syer @@ -80,8 +79,8 @@ * */ @RabbitAvailable(queues = CachingConnectionFactoryIntegrationTests.CF_INTEGRATION_TEST_QUEUE) -@LogLevels(classes = { CachingConnectionFactoryIntegrationTests.class, - CachingConnectionFactory.class }, categories = "com.rabbitmq", level = "DEBUG") +@LogLevels(classes = {CachingConnectionFactoryIntegrationTests.class, + CachingConnectionFactory.class}, categories = "com.rabbitmq", level = "DEBUG") public class CachingConnectionFactoryIntegrationTests { public static final String CF_INTEGRATION_TEST_QUEUE = "cfIntegrationTest"; @@ -269,16 +268,16 @@ public void testReceiveFromNonExistentVirtualHost() { RabbitTemplate template = new RabbitTemplate(connectionFactory); assertThatThrownBy(() -> template.receiveAndConvert("foo")) - .isInstanceOfAny( - // Wrong vhost is very unfriendly to client - the exception has no clue (just an EOF) - AmqpIOException.class, - AmqpAuthenticationException.class, - /* - * If localhost also resolves to an IPv6 address, the client will try that - * after a failure due to an invalid vHost and, if Rabbit is not listening there, - * we'll get an... - */ - AmqpConnectException.class); + .isInstanceOfAny( + // Wrong vhost is very unfriendly to client - the exception has no clue (just an EOF) + AmqpIOException.class, + AmqpAuthenticationException.class, + /* + * If localhost also resolves to an IPv6 address, the client will try that + * after a failure due to an invalid vHost and, if Rabbit is not listening there, + * we'll get an... + */ + AmqpConnectException.class); } @Test @@ -295,7 +294,7 @@ public void testSendAndReceiveFromVolatileQueueAfterImplicitRemoval() throws Exc // The queue was removed when the channel was closed assertThatThrownBy(() -> template.receiveAndConvert(queue.getName())) - .isInstanceOf(AmqpIOException.class); + .isInstanceOf(AmqpIOException.class); template.stop(); } @@ -315,11 +314,11 @@ public void testMixTransactionalAndNonTransactional() throws Exception { // The channel is not transactional assertThatThrownBy(() -> - template2.execute(channel -> { - // Should be an exception because the channel is not transactional - channel.txRollback(); - return null; - })).isInstanceOf(AmqpIOException.class); + template2.execute(channel -> { + // Should be an exception because the channel is not transactional + channel.txRollback(); + return null; + })).isInstanceOf(AmqpIOException.class); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java index 8ecddfcb40..7746bf955d 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -16,30 +16,6 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assertions.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.BDDMockito.willReturn; -import static org.mockito.BDDMockito.willThrow; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; - import java.io.IOException; import java.net.URI; import java.util.ArrayList; @@ -63,6 +39,13 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; +import com.rabbitmq.client.Address; +import com.rabbitmq.client.AddressResolver; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.ConfirmListener; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.GetResponse; +import com.rabbitmq.client.ShutdownSignalException; import org.apache.commons.logging.Log; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -81,13 +64,29 @@ import org.springframework.context.event.ContextClosedEvent; import org.springframework.test.util.ReflectionTestUtils; -import com.rabbitmq.client.Address; -import com.rabbitmq.client.AddressResolver; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.ConfirmListener; -import com.rabbitmq.client.ConnectionFactory; -import com.rabbitmq.client.GetResponse; -import com.rabbitmq.client.ShutdownSignalException; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; /** * @author Mark Pollack diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ClientRecoveryCompatibilityTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ClientRecoveryCompatibilityTests.java index 629bfa4bfe..bb04708c26 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ClientRecoveryCompatibilityTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ClientRecoveryCompatibilityTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * Copyright 2014-2025 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. @@ -16,6 +16,12 @@ package org.springframework.amqp.rabbit.connection; +import java.util.concurrent.ExecutorService; + +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.impl.recovery.AutorecoveringConnection; +import org.junit.jupiter.api.Test; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.any; @@ -26,13 +32,6 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import java.util.concurrent.ExecutorService; - -import org.junit.jupiter.api.Test; - -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.impl.recovery.AutorecoveringConnection; - /** * @author Gary Russell * @author Artem Bilan diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryContextWrapperTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryContextWrapperTests.java index 26637881f3..20cf120595 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryContextWrapperTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryContextWrapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2025 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. @@ -16,16 +16,16 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - import java.util.concurrent.Callable; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + /** * @author Wander Costa */ diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryLifecycleTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryLifecycleTests.java index 1b8c67aa11..2ed613e970 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryLifecycleTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryLifecycleTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-2025 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. @@ -16,13 +16,14 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.BlockedListener; +import com.rabbitmq.client.impl.AMQCommand; +import com.rabbitmq.client.impl.AMQConnection; +import com.rabbitmq.client.impl.AMQImpl; import org.junit.jupiter.api.Test; import org.springframework.amqp.AmqpApplicationContextClosedException; @@ -38,10 +39,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import com.rabbitmq.client.BlockedListener; -import com.rabbitmq.client.impl.AMQCommand; -import com.rabbitmq.client.impl.AMQConnection; -import com.rabbitmq.client.impl.AMQImpl; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryUtilsTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryUtilsTests.java index a59ca09443..b2de758b2c 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryUtilsTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 the original author or authors. + * Copyright 2017-2025 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. @@ -16,13 +16,13 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - import org.junit.jupiter.api.Test; import org.springframework.transaction.support.TransactionSynchronizationManager; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + /** * @author Gary Russell * @since 1.7.1 diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionListenerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionListenerTests.java index 606c71cf4c..8d94733dd1 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionListenerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConnectionListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2025 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. @@ -16,15 +16,15 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - import java.util.concurrent.atomic.AtomicBoolean; import org.junit.jupiter.api.Test; import org.springframework.amqp.AmqpIOException; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + /** * @author Gary Russell * @author DongMin Park diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConsumerConnectionRecoveryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConsumerConnectionRecoveryTests.java index 1ddf92fa74..9ce3267893 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConsumerConnectionRecoveryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ConsumerConnectionRecoveryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2023-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; @@ -40,6 +38,8 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Artem Bilan diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryIntegrationTests.java index a82307863f..691d4d9c26 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2022 the original author or authors. + * Copyright 2015-2025 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. @@ -16,9 +16,6 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; @@ -41,6 +38,8 @@ import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.util.UriUtils; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * @@ -67,10 +66,10 @@ public void setup() { this.testContainerAdmin = new RabbitAdmin(this.testContainerFactory); BrokerRunningSupport brokerRunning = RabbitAvailableCondition.getBrokerRunning(); - String[] addresses = new String[] { brokerRunning.getHostName() + ":" + brokerRunning.getPort(), - RABBITMQ.getHost() + ":" + RABBITMQ.getAmqpPort() }; - String[] adminUris = new String[] { brokerRunning.getAdminUri(), RABBITMQ.getHttpUrl() }; - String[] nodes = new String[] { findLocalNode(), findTcNode() }; + String[] addresses = new String[] {brokerRunning.getHostName() + ":" + brokerRunning.getPort(), + RABBITMQ.getHost() + ":" + RABBITMQ.getAmqpPort()}; + String[] adminUris = new String[] {brokerRunning.getAdminUri(), RABBITMQ.getHttpUrl()}; + String[] nodes = new String[] {findLocalNode(), findTcNode()}; String vhost = "/"; String username = brokerRunning.getAdminUser(); String password = brokerRunning.getAdminPassword(); @@ -108,7 +107,7 @@ void findLocal() { BrokerRunningSupport brokerRunning = RabbitAvailableCondition.getBrokerRunning(); LocalizedQueueConnectionFactory lqcf = new LocalizedQueueConnectionFactory(defaultCf, Map.of(findLocalNode(), brokerRunning.getHostName() + ":" + brokerRunning.getPort()), - new String[] { brokerRunning.getAdminUri() }, + new String[] {brokerRunning.getAdminUri()}, "/", brokerRunning.getAdminUser(), brokerRunning.getAdminPassword(), false, null); ConnectionFactory cf = lqcf.getTargetConnectionFactory("[local]"); RabbitAdmin admin = new RabbitAdmin(cf); @@ -140,6 +139,7 @@ private String findTcNode() { .accept(MediaType.APPLICATION_JSON) .retrieve() .bodyToMono(new ParameterizedTypeReference>() { + }) .block(Duration.ofSeconds(10)); this.testContainerAdmin.deleteQueue(queue.getName()); @@ -168,6 +168,7 @@ private String findLocalNode() { .accept(MediaType.APPLICATION_JSON) .retrieve() .bodyToMono(new ParameterizedTypeReference>() { + }) .block(Duration.ofSeconds(10)); this.defaultAdmin.deleteQueue(queue.getName()); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryTests.java index 29192e6a62..b746628925 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 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. @@ -16,19 +16,6 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.BDDMockito.willReturn; -import static org.mockito.Mockito.atLeast; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - import java.util.HashMap; import java.util.List; import java.util.Map; @@ -37,10 +24,13 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Consumer; import org.apache.commons.logging.Log; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.internal.stubbing.answers.CallsRealMethods; +import reactor.core.publisher.Mono; import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; import org.springframework.amqp.utils.test.TestUtils; @@ -52,10 +42,18 @@ import org.springframework.test.web.reactive.server.HttpHandlerConnector; import org.springframework.web.reactive.function.client.WebClient; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Consumer; -import reactor.core.publisher.Mono; - +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; /** * @author Gary Russell @@ -74,9 +72,9 @@ public void testFailOver() throws Exception { ConnectionFactory defaultConnectionFactory = mockCF("localhost:1234", null); String rabbit1 = "localhost:1235"; String rabbit2 = "localhost:1236"; - String[] addresses = new String[]{rabbit1, rabbit2}; - String[] adminUris = new String[]{"http://localhost:11235", "http://localhost:11236"}; - String[] nodes = new String[]{"rabbit@foo", "rabbit@bar"}; + String[] addresses = new String[] {rabbit1, rabbit2}; + String[] adminUris = new String[] {"http://localhost:11235", "http://localhost:11236"}; + String[] nodes = new String[] {"rabbit@foo", "rabbit@bar"}; String vhost = "/"; String username = "guest"; String password = "guest"; @@ -174,9 +172,9 @@ public void test2Queues() throws Exception { try { String rabbit1 = "localhost:1235"; String rabbit2 = "localhost:1236"; - String[] addresses = new String[]{rabbit1, rabbit2}; - String[] adminUris = new String[]{"http://localhost:11235", "http://localhost:11236"}; - String[] nodes = new String[]{"rabbit@foo", "rabbit@bar"}; + String[] addresses = new String[] {rabbit1, rabbit2}; + String[] adminUris = new String[] {"http://localhost:11235", "http://localhost:11236"}; + String[] nodes = new String[] {"rabbit@foo", "rabbit@bar"}; String vhost = "/"; String username = "guest"; String password = "guest"; diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/NodeLocatorTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/NodeLocatorTests.java index cd802b4231..9657dd1d18 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/NodeLocatorTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/NodeLocatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2025 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. @@ -16,11 +16,6 @@ package org.springframework.amqp.rabbit.connection; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - import java.net.URISyntaxException; import java.util.Map; @@ -29,6 +24,11 @@ import org.springframework.lang.Nullable; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + /** * @author Gary Russell * @since 3.0 diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactoryTests.java index 2c94b9be6f..b1f747010a 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-2025 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. @@ -16,14 +16,14 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.ConnectionFactory; import org.apache.commons.pool2.impl.GenericObjectPool; import org.junit.jupiter.api.Test; @@ -38,8 +38,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.test.util.ReflectionTestUtils; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.ConnectionFactory; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelTests.java index 6872f3f96e..743f5f95cf 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 the original author or authors. + * Copyright 2019-2025 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. @@ -16,13 +16,6 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.Mockito.mock; - import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -38,6 +31,13 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.IntStream; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.LongString; +import com.rabbitmq.client.Method; +import com.rabbitmq.client.Return; +import com.rabbitmq.client.ShutdownListener; +import com.rabbitmq.client.ShutdownSignalException; import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory.ConfirmType; @@ -48,13 +48,12 @@ import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; import org.springframework.core.task.SimpleAsyncTaskExecutor; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.LongString; -import com.rabbitmq.client.Method; -import com.rabbitmq.client.Return; -import com.rabbitmq.client.ShutdownListener; -import com.rabbitmq.client.ShutdownSignalException; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/RabbitReconnectProblemTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/RabbitReconnectProblemTests.java index a1dbbb7b51..3c9cde97d1 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/RabbitReconnectProblemTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/RabbitReconnectProblemTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 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. @@ -16,11 +16,11 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; - +import java.io.PrintStream; import java.util.Map; import java.util.concurrent.Semaphore; +import com.rabbitmq.client.ConnectionFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -36,7 +36,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import com.rabbitmq.client.ConnectionFactory; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Lars Hvile @@ -48,6 +48,8 @@ @Disabled("Requires user interaction") public class RabbitReconnectProblemTests { + private static final PrintStream SOUT = System.out; + @Autowired CachingConnectionFactory connFactory; @@ -67,7 +69,7 @@ public void setup() { @Test public void surviveAReconnect() throws Exception { checkIt(0); - System .out .println("Restart RabbitMQ & press any key..."); + SOUT.println("Restart RabbitMQ & press any key..."); System.in.read(); for (int i = 1; i < 10; i++) { @@ -79,14 +81,14 @@ public void surviveAReconnect() throws Exception { .iterator() .next()) .availablePermits(); - System .out .println("Permits after test: " + availablePermits); + SOUT.println("Permits after test: " + availablePermits); assertThat(availablePermits).isEqualTo(2); } void checkIt(int counter) { - System .out .println("\n#" + counter); + SOUT.println("\n#" + counter); template.receive(myQueue.getName()); - System .out .println("OK"); + SOUT.println("OK"); } @Configuration @@ -113,5 +115,7 @@ AmqpAdmin rabbitAdmin() throws Exception { AmqpTemplate rabbitTemplate() throws Exception { return new RabbitTemplate(connectionFactory()); } + } + } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/RoutingConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/RoutingConnectionFactoryTests.java index 6c6b7f5f32..b190a856cf 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/RoutingConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/RoutingConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -16,18 +16,6 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNoException; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - import java.util.HashMap; import java.util.Map; import java.util.concurrent.CountDownLatch; @@ -40,6 +28,7 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import com.rabbitmq.client.Channel; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -48,7 +37,17 @@ import org.springframework.amqp.rabbit.listener.DirectReplyToMessageListenerContainer.ChannelHolder; import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; /** * @author Artem Bilan diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SSLConnectionTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SSLConnectionTests.java index e4e4f926be..9f9c84475d 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SSLConnectionTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SSLConnectionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 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. @@ -16,19 +16,16 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - import java.security.SecureRandom; import java.util.Collections; import javax.net.ssl.SSLContext; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.impl.CredentialsProvider; +import com.rabbitmq.client.impl.CredentialsRefreshService; import org.apache.commons.logging.Log; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -39,11 +36,13 @@ import org.springframework.beans.DirectFieldAccessor; import org.springframework.core.io.ClassPathResource; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Connection; -import com.rabbitmq.client.ConnectionFactory; -import com.rabbitmq.client.impl.CredentialsProvider; -import com.rabbitmq.client.impl.CredentialsRefreshService; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SingleConnectionFactory.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SingleConnectionFactory.java index 1fe0e11b9c..085ba77ecc 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SingleConnectionFactory.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SingleConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -19,12 +19,12 @@ import java.net.URI; import java.util.List; -import org.springframework.amqp.AmqpException; -import org.springframework.util.StringUtils; - import com.rabbitmq.client.BlockedListener; import com.rabbitmq.client.Channel; +import org.springframework.amqp.AmqpException; +import org.springframework.util.StringUtils; + /** * A {@link ConnectionFactory} implementation that returns the same Connections from all {@link #createConnection()} * calls, and ignores calls to {@link com.rabbitmq.client.Connection#close()}. diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SingleConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SingleConnectionFactoryTests.java index b1fe3753cc..e97ac76794 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SingleConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SingleConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 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. @@ -16,6 +16,14 @@ package org.springframework.amqp.rabbit.connection; +import java.util.Collections; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicInteger; + +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.ConnectionFactory; +import org.junit.jupiter.api.Test; + import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -26,15 +34,6 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import java.util.Collections; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.jupiter.api.Test; - -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.ConnectionFactory; - /** * @author Dave Syer * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactoryTests.java index 5b34ec4649..1e9105a599 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-2025 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. @@ -16,12 +16,6 @@ package org.springframework.amqp.rabbit.connection; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - import java.io.IOException; import java.util.Map; import java.util.concurrent.BlockingQueue; @@ -30,6 +24,9 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.ConnectionFactory; import org.apache.commons.logging.Log; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -50,9 +47,11 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.ConnectionFactory; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplateTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplateTests.java index b554fdd14c..2fd8d9db83 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplateTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 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. @@ -16,16 +16,6 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.willDoNothing; -import static org.mockito.BDDMockito.willReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - import java.io.OutputStream; import java.lang.reflect.Method; import java.time.Duration; @@ -37,6 +27,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.zip.Deflater; +import com.rabbitmq.client.ConnectionFactory; import org.apache.commons.logging.Log; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -76,7 +67,15 @@ import org.springframework.util.ReflectionUtils; import org.springframework.util.StopWatch; -import com.rabbitmq.client.ConnectionFactory; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/FixedReplyQueueDeadLetterTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/FixedReplyQueueDeadLetterTests.java index f4145c41d3..8be0ff1d53 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/FixedReplyQueueDeadLetterTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/FixedReplyQueueDeadLetterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 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. @@ -16,9 +16,6 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; - import java.net.MalformedURLException; import java.net.URISyntaxException; import java.util.Map; @@ -47,6 +44,9 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + /** * * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/MessagingTemplateConfirmsTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/MessagingTemplateConfirmsTests.java index 51a70bb93d..d537784d34 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/MessagingTemplateConfirmsTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/MessagingTemplateConfirmsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Collections; import java.util.concurrent.TimeUnit; @@ -31,6 +29,8 @@ import org.springframework.amqp.support.AmqpHeaders; import org.springframework.messaging.support.GenericMessage; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * @since 2.3 diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminDeclarationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminDeclarationTests.java index a6d04075b3..4db98465b8 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminDeclarationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminDeclarationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -16,27 +16,14 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.BDDMockito.willReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.impl.AMQImpl; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.AmqpAdmin; @@ -56,8 +43,20 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.support.GenericApplicationContext; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.impl.AMQImpl; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminIntegrationTests.java index aa2f95f648..47b072f210 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -16,17 +16,16 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNoException; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.awaitility.Awaitility.await; - import java.io.IOException; import java.time.Duration; import java.util.Map; import java.util.Objects; import java.util.UUID; +import com.rabbitmq.client.AMQP.Queue.DeclareOk; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -50,10 +49,10 @@ import org.springframework.amqp.rabbit.junit.RabbitAvailable; import org.springframework.context.support.GenericApplicationContext; -import com.rabbitmq.client.AMQP.Queue.DeclareOk; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Connection; -import com.rabbitmq.client.ConnectionFactory; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.awaitility.Awaitility.await; /** * @author Dave Syer diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java index c1c19b4945..07465a9d10 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 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. @@ -16,25 +16,6 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assertions.fail; -import static org.awaitility.Awaitility.await; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willDoNothing; -import static org.mockito.BDDMockito.willReturn; -import static org.mockito.BDDMockito.willThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; - import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -46,6 +27,9 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeoutException; +import com.rabbitmq.client.AMQP.Queue.DeclareOk; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.DefaultConsumer; import org.apache.commons.logging.Log; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -82,9 +66,24 @@ import org.springframework.retry.backoff.NoBackOffPolicy; import org.springframework.retry.support.RetryTemplate; -import com.rabbitmq.client.AMQP.Queue.DeclareOk; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.DefaultConsumer; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; /** * @author Mark Pollack diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitBindingIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitBindingIntegrationTests.java index 317d06f220..867d6b455f 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitBindingIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitBindingIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 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. @@ -16,9 +16,6 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; - import java.time.Duration; import org.junit.jupiter.api.AfterEach; @@ -40,6 +37,9 @@ import org.springframework.amqp.rabbit.support.DefaultMessagePropertiesConverter; import org.springframework.amqp.support.converter.SimpleMessageConverter; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + /** * @author Dave Syer * @author Gunnar Hillert diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitGatewaySupportTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitGatewaySupportTests.java index 4d29a8340f..e740e054e0 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitGatewaySupportTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitGatewaySupportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2019 the original author or authors. + * Copyright 2010-2025 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. @@ -16,14 +16,14 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + /** * @author Mark Pollack * @author Chris Beams diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplateTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplateTests.java index 4544864383..d14e75815c 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplateTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 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. @@ -16,17 +16,6 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - import java.io.Writer; import java.util.Collections; import java.util.HashMap; @@ -55,6 +44,17 @@ import org.springframework.messaging.support.GenericMessage; import org.springframework.messaging.support.MessageBuilder; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + /** * @author Stephane Nicoll diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateDirectReplyToContainerIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateDirectReplyToContainerIntegrationTests.java index 1b6102908d..2dd13583d8 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateDirectReplyToContainerIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateDirectReplyToContainerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 the original author or authors. + * Copyright 2016-2025 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. @@ -16,10 +16,6 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; - import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; @@ -27,6 +23,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.Channel; import org.junit.jupiter.api.Test; import org.springframework.amqp.AmqpRejectAndDontRequeueException; @@ -38,7 +35,9 @@ import org.springframework.amqp.utils.test.TestUtils; import org.springframework.util.ErrorHandler; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateHeaderTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateHeaderTests.java index 8754a999d7..d5af8747b4 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateHeaderTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateHeaderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 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. @@ -16,19 +16,16 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.Mockito.mock; - import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -37,10 +34,12 @@ import org.springframework.amqp.rabbit.connection.SingleConnectionFactory; import org.springframework.amqp.rabbit.support.DefaultMessagePropertiesConverter; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Connection; -import com.rabbitmq.client.ConnectionFactory; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateIntegrationTests.java index 06109d6606..10abeb6c3c 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -16,22 +16,6 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assertions.fail; -import static org.awaitility.Awaitility.await; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.BDDMockito.willReturn; -import static org.mockito.Mockito.atLeast; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - import java.io.IOException; import java.lang.reflect.Field; import java.util.Arrays; @@ -52,6 +36,16 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.AlreadyClosedException; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Consumer; +import com.rabbitmq.client.Envelope; +import com.rabbitmq.client.GetResponse; +import com.rabbitmq.client.Method; +import com.rabbitmq.client.ShutdownSignalException; +import com.rabbitmq.client.impl.AMQImpl; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.AfterEach; @@ -123,16 +117,21 @@ import org.springframework.transaction.support.TransactionTemplate; import org.springframework.util.ReflectionUtils; -import com.rabbitmq.client.AMQP; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.AlreadyClosedException; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Consumer; -import com.rabbitmq.client.Envelope; -import com.rabbitmq.client.GetResponse; -import com.rabbitmq.client.Method; -import com.rabbitmq.client.ShutdownSignalException; -import com.rabbitmq.client.impl.AMQImpl; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; /** * @author Dave Syer diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateMPPIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateMPPIntegrationTests.java index 97f37a3e8b..55aabab021 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateMPPIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateMPPIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 the original author or authors. + * Copyright 2017-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; - import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Message; @@ -37,6 +35,8 @@ import org.springframework.test.annotation.DirtiesContext.ClassMode; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * @since 1.7.6 diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePerformanceIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePerformanceIntegrationTests.java index e4a5228a16..892a55c67f 100755 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePerformanceIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePerformanceIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -41,6 +39,8 @@ import org.springframework.transaction.support.DefaultTransactionStatus; import org.springframework.transaction.support.TransactionTemplate; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Dave Syer * @author Gunnar Hillert diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration1Tests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration1Tests.java index 1553c12c4d..1c0fda2a71 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration1Tests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration1Tests.java @@ -16,20 +16,6 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.BDDMockito.willReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; @@ -50,6 +36,9 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; import org.apache.commons.logging.Log; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.AfterEach; @@ -84,9 +73,19 @@ import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Connection; -import com.rabbitmq.client.ConnectionFactory; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration2Tests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration2Tests.java index 06837b9895..a824830537 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration2Tests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration2Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 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. @@ -16,11 +16,12 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.DefaultConsumer; +import com.rabbitmq.client.Envelope; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -32,9 +33,7 @@ import org.springframework.amqp.rabbit.junit.BrokerTestUtils; import org.springframework.amqp.rabbit.junit.RabbitAvailable; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.DefaultConsumer; -import com.rabbitmq.client.Envelope; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration3Tests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration3Tests.java index a04f4338cf..2fcfc6ede4 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration3Tests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration3Tests.java @@ -16,15 +16,13 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import com.rabbitmq.client.Channel; import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; @@ -37,7 +35,8 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.event.ContextClosedEvent; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateRoutingConnectionFactoryIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateRoutingConnectionFactoryIntegrationTests.java index b2ebbbb7f1..f99e30d7b2 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateRoutingConnectionFactoryIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateRoutingConnectionFactoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; - import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.HashMap; @@ -42,6 +40,8 @@ import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Leonardo Ferreira * @since 2.4.4 diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateTests.java index 5e6fb2a6d3..23dfe6a348 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -16,25 +16,6 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assertions.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.BDDMockito.willReturn; -import static org.mockito.BDDMockito.willThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - import java.io.IOException; import java.util.Collection; import java.util.Collections; @@ -49,6 +30,18 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.AMQP.Tx.SelectOk; +import com.rabbitmq.client.AuthenticationFailureException; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.Consumer; +import com.rabbitmq.client.Envelope; +import com.rabbitmq.client.ShutdownListener; +import com.rabbitmq.client.ShutdownSignalException; +import com.rabbitmq.client.impl.AMQImpl; +import com.rabbitmq.client.impl.AMQImpl.Queue.DeclareOk; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; @@ -88,18 +81,24 @@ import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.transaction.support.TransactionTemplate; -import com.rabbitmq.client.AMQP; -import com.rabbitmq.client.AMQP.Tx.SelectOk; -import com.rabbitmq.client.AuthenticationFailureException; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Connection; -import com.rabbitmq.client.ConnectionFactory; -import com.rabbitmq.client.Consumer; -import com.rabbitmq.client.Envelope; -import com.rabbitmq.client.ShutdownListener; -import com.rabbitmq.client.ShutdownSignalException; -import com.rabbitmq.client.impl.AMQImpl; -import com.rabbitmq.client.impl.AMQImpl.Queue.DeclareOk; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/SimplePublisherConfirmsTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/SimplePublisherConfirmsTests.java index 3f5714b5c4..30a298bdca 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/SimplePublisherConfirmsTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/SimplePublisherConfirmsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; @@ -28,6 +26,8 @@ import org.springframework.amqp.rabbit.connection.CachingConnectionFactory.ConfirmType; import org.springframework.amqp.rabbit.junit.RabbitAvailable; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/TransactionalEventListenerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/TransactionalEventListenerTests.java index 7885cace48..2691b15385 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/TransactionalEventListenerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/TransactionalEventListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2025 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. @@ -16,14 +16,10 @@ package org.springframework.amqp.rabbit.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - import java.io.IOException; import java.util.concurrent.atomic.AtomicBoolean; +import com.rabbitmq.client.Channel; import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.connection.Connection; @@ -44,7 +40,10 @@ import org.springframework.transaction.support.AbstractPlatformTransactionManager; import org.springframework.transaction.support.DefaultTransactionStatus; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/AsyncReplyToTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/AsyncReplyToTests.java index 23e6bb17d5..06e6a194fa 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/AsyncReplyToTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/AsyncReplyToTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 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. @@ -16,14 +16,13 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; - import java.io.IOException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import com.rabbitmq.client.Channel; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.AcknowledgeMode; @@ -47,7 +46,7 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumerIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumerIntegrationTests.java index 87cc18c71f..bd314d932c 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumerIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -16,9 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -38,6 +35,9 @@ import org.springframework.amqp.rabbit.support.ActiveObjectCounter; import org.springframework.amqp.rabbit.support.DefaultMessagePropertiesConverter; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + /** * @author Dave Syer * @author Gunnar Hillert diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumerTests.java index 17c9d893f1..a57c643a23 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 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. @@ -16,20 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.BDDMockito.willReturn; -import static org.mockito.BDDMockito.willThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - import java.io.IOException; import java.util.HashSet; import java.util.Map; @@ -43,6 +29,13 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Consumer; +import com.rabbitmq.client.Envelope; +import com.rabbitmq.client.ShutdownSignalException; +import com.rabbitmq.client.impl.recovery.AutorecoveringChannel; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -59,13 +52,19 @@ import org.springframework.amqp.utils.test.TestUtils; import org.springframework.beans.DirectFieldAccessor; -import com.rabbitmq.client.AMQP; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Consumer; -import com.rabbitmq.client.Envelope; -import com.rabbitmq.client.ShutdownSignalException; -import com.rabbitmq.client.impl.recovery.AutorecoveringChannel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BrokerDeclaredQueueNameTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BrokerDeclaredQueueNameTests.java index 434621ee2f..f86aeddab3 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BrokerDeclaredQueueNameTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BrokerDeclaredQueueNameTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 the original author or authors. + * Copyright 2018-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -42,6 +40,8 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BrokerEventListenerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BrokerEventListenerTests.java index d5473bc3cb..37df43223c 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BrokerEventListenerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/BrokerEventListenerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 the original author or authors. + * Copyright 2018-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.HashMap; import java.util.Map; import java.util.concurrent.CountDownLatch; @@ -41,6 +39,8 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * @since 2.1 diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerAdminTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerAdminTests.java index 22ef6ae445..3fcf53d8bf 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerAdminTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerAdminTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; - import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; @@ -27,6 +25,8 @@ import org.springframework.amqp.utils.test.TestUtils; import org.springframework.context.support.GenericApplicationContext; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * @since 2.4 diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerInitializationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerInitializationTests.java index c0b87bed74..16f6acdac5 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerInitializationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerInitializationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 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. @@ -16,10 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; -import static org.awaitility.Awaitility.await; - import java.time.Duration; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -42,6 +38,10 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.awaitility.Awaitility.await; + /** * @author Gary Russell * @since 1.6 diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerShutDownTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerShutDownTests.java index 1902921ad9..aec9f33cad 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerShutDownTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerShutDownTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2024 the original author or authors. + * Copyright 2017-2025 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. @@ -16,12 +16,11 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import com.rabbitmq.client.AMQP.BasicProperties; import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; @@ -32,7 +31,7 @@ import org.springframework.amqp.utils.test.TestUtils; import org.springframework.util.StopWatch; -import com.rabbitmq.client.AMQP.BasicProperties; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerUtilsTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerUtilsTests.java index fd91a23942..3c456a2055 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerUtilsTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ContainerUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-2025 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. @@ -16,9 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - import org.apache.commons.logging.Log; import org.junit.jupiter.api.Test; @@ -27,6 +24,9 @@ import org.springframework.amqp.rabbit.listener.support.ContainerUtils; import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + /** * @author Gary Russell * @since 2.1.8 diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java index 8fe662f17c..11b7feea7b 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 the original author or authors. + * Copyright 2016-2025 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. @@ -16,18 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.awaitility.Awaitility.await; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - import java.net.UnknownHostException; import java.time.Duration; import java.util.ArrayList; @@ -40,6 +28,8 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Consumer; import org.aopalliance.intercept.MethodInterceptor; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -78,8 +68,17 @@ import org.springframework.util.MultiValueMap; import org.springframework.util.backoff.FixedBackOff; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Consumer; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerMockTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerMockTests.java index 16580d5184..f3a710ab21 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerMockTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainerMockTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 the original author or authors. + * Copyright 2017-2025 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. @@ -16,21 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.BDDMockito.willThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -38,6 +23,12 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Consumer; +import com.rabbitmq.client.Envelope; +import com.rabbitmq.client.impl.recovery.AutorecoveringChannel; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -47,12 +38,20 @@ import org.springframework.amqp.rabbit.connection.Connection; import org.springframework.amqp.rabbit.connection.ConnectionFactory; -import com.rabbitmq.client.AMQP; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Consumer; -import com.rabbitmq.client.Envelope; -import com.rabbitmq.client.impl.recovery.AutorecoveringChannel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainerTests.java index 20bd2e6b98..99de689327 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 the original author or authors. + * Copyright 2016-2025 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. @@ -16,14 +16,14 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; - import java.time.Duration; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.GetResponse; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Address; @@ -34,9 +34,8 @@ import org.springframework.amqp.utils.test.TestUtils; import org.springframework.beans.DirectFieldAccessor; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.GetResponse; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; /** * DirectReplyToMessageListenerContainer Tests. @@ -57,7 +56,8 @@ public void testReleaseConsumerRace() throws Exception { final CountDownLatch latch = new CountDownLatch(1); // Populate void MessageListener for wrapping in the DirectReplyToMessageListenerContainer - container.setMessageListener(m -> { }); + container.setMessageListener(m -> { + }); // Extract actual ChannelAwareMessageListener from container // with the inUseConsumerChannels.remove(channel); operation diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DlqExpiryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DlqExpiryTests.java index 4b5d8ca51b..f5639fce06 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DlqExpiryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/DlqExpiryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 the original author or authors. + * Copyright 2018-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -41,6 +39,8 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * @since 2.1 diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ErrorHandlerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ErrorHandlerTests.java index 6ae1f35f18..1fe24db479 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ErrorHandlerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ErrorHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 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. @@ -16,13 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.willDoNothing; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; - import org.apache.commons.logging.Log; import org.junit.jupiter.api.Test; @@ -38,6 +31,13 @@ import org.springframework.messaging.handler.annotation.support.MethodArgumentNotValidException; import org.springframework.messaging.handler.annotation.support.MethodArgumentTypeMismatchException; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; + /** * @author Gary Russell * @author Artem Bilan diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ExternalTxManagerSMLCTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ExternalTxManagerSMLCTests.java index 44350d83e0..07819e5b44 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ExternalTxManagerSMLCTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ExternalTxManagerSMLCTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 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. @@ -16,23 +16,17 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.Consumer; +import com.rabbitmq.client.Envelope; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -42,12 +36,17 @@ import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.context.ApplicationEventPublisher; -import com.rabbitmq.client.AMQP; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Connection; -import com.rabbitmq.client.ConnectionFactory; -import com.rabbitmq.client.Consumer; -import com.rabbitmq.client.Envelope; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ExternalTxManagerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ExternalTxManagerTests.java index dedb34a915..f24cabcc06 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ExternalTxManagerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ExternalTxManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -16,19 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -37,6 +24,12 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.Consumer; +import com.rabbitmq.client.Envelope; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.mockito.stubbing.Answer; @@ -60,12 +53,18 @@ import org.springframework.transaction.support.AbstractPlatformTransactionManager; import org.springframework.transaction.support.DefaultTransactionStatus; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Connection; -import com.rabbitmq.client.ConnectionFactory; -import com.rabbitmq.client.Consumer; -import com.rabbitmq.client.Envelope; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/JavaConfigFixedReplyQueueTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/JavaConfigFixedReplyQueueTests.java index d795411f46..ffb379973b 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/JavaConfigFixedReplyQueueTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/JavaConfigFixedReplyQueueTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 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. @@ -16,9 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - import java.util.Arrays; import java.util.UUID; @@ -46,6 +43,9 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + /** * NOTE: This class is referenced in the reference documentation; if it is changed/moved, be * sure to update that documentation. diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ListenFromAutoDeleteQueueTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ListenFromAutoDeleteQueueTests.java index 88be38d8f8..a560ceb652 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ListenFromAutoDeleteQueueTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ListenFromAutoDeleteQueueTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * Copyright 2014-2025 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. @@ -16,17 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willThrow; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; @@ -46,6 +35,17 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + /** * @author Gary Russell * @author Artem Bilan diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/LocallyTransactedTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/LocallyTransactedTests.java index 9ed4a913e5..34b5f1a678 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/LocallyTransactedTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/LocallyTransactedTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -16,19 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; @@ -36,6 +23,12 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.Consumer; +import com.rabbitmq.client.Envelope; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; @@ -50,12 +43,18 @@ import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener; import org.springframework.beans.DirectFieldAccessor; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Connection; -import com.rabbitmq.client.ConnectionFactory; -import com.rabbitmq.client.Consumer; -import com.rabbitmq.client.Envelope; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerErrorHandlerIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerErrorHandlerIntegrationTests.java index 1d28abe78b..28e469cf4c 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerErrorHandlerIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerErrorHandlerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -16,23 +16,12 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; -import static org.awaitility.Awaitility.await; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.contains; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.BDDMockito.willReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.Channel; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.BeforeEach; @@ -63,7 +52,17 @@ import org.springframework.beans.factory.DisposableBean; import org.springframework.util.ErrorHandler; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; /** * @author Dave Syer diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerLifecycleIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerLifecycleIntegrationTests.java index cb53e80b23..2332ea590d 100755 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerLifecycleIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerLifecycleIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -16,13 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.awaitility.Awaitility.await; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.Mockito.spy; - import java.net.UnknownHostException; import java.util.Set; import java.util.concurrent.BlockingQueue; @@ -31,6 +24,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import com.rabbitmq.client.DnsRecordIpAddressResolver; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.Test; @@ -58,7 +52,12 @@ import org.springframework.context.annotation.Configuration; import org.springframework.test.context.junit.jupiter.DisabledIf; -import com.rabbitmq.client.DnsRecordIpAddressResolver; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.awaitility.Awaitility.await; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.spy; /** * @author Dave Syer diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerMultipleQueueIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerMultipleQueueIntegrationTests.java index d2fe778b84..8e1e64ca62 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerMultipleQueueIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerMultipleQueueIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -36,6 +34,8 @@ import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter; import org.springframework.amqp.support.converter.SimpleMessageConverter; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Mark Fisher * @author Gunnar Hillert diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerRetryIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerRetryIntegrationTests.java index 576ac75a8e..e958d10469 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerRetryIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerRetryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -16,9 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -49,6 +46,9 @@ import org.springframework.retry.policy.MapRetryContextCache; import org.springframework.retry.support.RetryTemplate; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + /** * @author Dave Syer * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerTxSynchTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerTxSynchTests.java index cc0b9f8c72..c2b02ff07b 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerTxSynchTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerTxSynchTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 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. @@ -16,15 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.Mockito.mock; - import java.io.IOException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; @@ -34,13 +25,6 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; -import org.junit.jupiter.api.Test; - -import org.springframework.amqp.rabbit.connection.SingleConnectionFactory; -import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; -import org.springframework.transaction.support.TransactionSynchronizationManager; - import com.rabbitmq.client.AMQP; import com.rabbitmq.client.AMQP.Tx.SelectOk; import com.rabbitmq.client.Channel; @@ -48,6 +32,21 @@ import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.Consumer; import com.rabbitmq.client.Envelope; +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.rabbit.connection.SingleConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerManualAckIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerManualAckIntegrationTests.java index c65a2008ec..fe8e786ff5 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerManualAckIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerManualAckIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -16,12 +16,10 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; - import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import com.rabbitmq.client.Channel; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.AfterEach; @@ -42,7 +40,8 @@ import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener; import org.springframework.beans.factory.DisposableBean; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; /** * @author Dave Syer diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerRecoveryCachingConnectionIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerRecoveryCachingConnectionIntegrationTests.java index 8cf6ffff0c..885a4185fc 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerRecoveryCachingConnectionIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerRecoveryCachingConnectionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 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. @@ -16,10 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.awaitility.Awaitility.with; - import java.time.Duration; import java.util.Collections; import java.util.HashSet; @@ -29,6 +25,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import com.rabbitmq.client.Channel; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.AfterEach; @@ -56,7 +53,9 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.support.GenericApplicationContext; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.awaitility.Awaitility.with; /** * @author Dave Syer diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerRecoveryRepeatIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerRecoveryRepeatIntegrationTests.java index 2ea14ec279..0081c73a78 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerRecoveryRepeatIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerRecoveryRepeatIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -16,13 +16,12 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import com.rabbitmq.client.Channel; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.AfterEach; @@ -46,7 +45,7 @@ import org.springframework.amqp.rabbit.listener.exception.FatalListenerExecutionException; import org.springframework.beans.factory.DisposableBean; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; /** * Long-running test created to facilitate profiling of SimpleMessageListenerContainer. diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerTxSizeIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerTxSizeIntegrationTests.java index 7a9df1b488..7fe5d3dbc6 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerTxSizeIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerTxSizeIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -16,11 +16,10 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import com.rabbitmq.client.Channel; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.AfterEach; @@ -39,7 +38,7 @@ import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener; import org.springframework.beans.factory.DisposableBean; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Dave Syer @@ -50,8 +49,8 @@ * */ @RabbitAvailable(queues = MessageListenerTxSizeIntegrationTests.TEST_QUEUE) -@LogLevels(level = "ERROR", classes = { RabbitTemplate.class, - SimpleMessageListenerContainer.class, BlockingQueueConsumer.class }) +@LogLevels(level = "ERROR", classes = {RabbitTemplate.class, + SimpleMessageListenerContainer.class, BlockingQueueConsumer.class}) public class MessageListenerTxSizeIntegrationTests { public static final String TEST_QUEUE = "test.queue.MessageListenerTxSizeIntegrationTests"; diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MethodRabbitListenerEndpointTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MethodRabbitListenerEndpointTests.java index 87d4dbff83..75b385308e 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MethodRabbitListenerEndpointTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MethodRabbitListenerEndpointTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 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. @@ -16,21 +16,14 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assertions.fail; -import static org.mockito.AdditionalMatchers.aryEq; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - import java.io.Serializable; import java.lang.reflect.Method; import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; @@ -60,8 +53,14 @@ import org.springframework.validation.Validator; import org.springframework.validation.annotation.Validated; -import com.rabbitmq.client.AMQP; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.AdditionalMatchers.aryEq; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MicrometerHolderTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MicrometerHolderTests.java index b576e507f3..4186f04f05 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MicrometerHolderTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MicrometerHolderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2025 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. @@ -16,13 +16,12 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.Mockito.mock; - import java.util.Collections; import java.util.Map; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.jupiter.api.Test; import org.springframework.context.ApplicationContext; @@ -31,9 +30,9 @@ import org.springframework.context.annotation.Primary; import org.springframework.test.util.ReflectionTestUtils; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Timer; -import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.Mockito.mock; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/QueueDeclarationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/QueueDeclarationTests.java index b963918c1a..72e424000b 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/QueueDeclarationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/QueueDeclarationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 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. @@ -16,17 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - import java.io.IOException; import java.util.HashMap; import java.util.List; @@ -35,6 +24,10 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Consumer; +import com.rabbitmq.client.impl.recovery.AutorecoveringChannel; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -46,10 +39,16 @@ import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.context.ApplicationContext; -import com.rabbitmq.client.AMQP; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Consumer; -import com.rabbitmq.client.impl.recovery.AutorecoveringChannel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistrarTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistrarTests.java index d4392ca93a..9150b4db84 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistrarTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistrarTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 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. @@ -16,10 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -27,6 +23,10 @@ import org.springframework.amqp.rabbit.config.SimpleRabbitListenerEndpoint; import org.springframework.beans.factory.support.StaticListableBeanFactory; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + /** * @author Stephane Nicoll * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistryTests.java index 5d22b5abc2..87e466e1f8 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -16,14 +16,14 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; - import org.junit.jupiter.api.Test; import org.springframework.amqp.rabbit.config.RabbitListenerContainerTestFactory; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerEndpoint; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + /** * * @author Stephane Nicoll diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java index c9fe35fa46..ea33a32397 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -16,17 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; -import static org.awaitility.Awaitility.with; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.BDDMockito.willReturn; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; - import java.io.IOException; import java.io.Serializable; import java.time.Duration; @@ -42,6 +31,8 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.AMQP.Queue.DeclareOk; +import com.rabbitmq.client.Channel; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.AfterEach; @@ -81,8 +72,16 @@ import org.springframework.core.log.LogMessage; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; -import com.rabbitmq.client.AMQP.Queue.DeclareOk; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.awaitility.Awaitility.with; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; /** * @author Dave Syer diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegrationTests.java index 64fee8a554..104ce4fd72 100755 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -16,10 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.awaitility.Awaitility.await; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -29,6 +25,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.Channel; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.logging.log4j.Level; @@ -59,7 +56,9 @@ import org.springframework.transaction.support.AbstractPlatformTransactionManager; import org.springframework.transaction.support.DefaultTransactionStatus; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.awaitility.Awaitility.await; /** * @author Dave Syer diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerLongTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerLongTests.java index 0a0a1641c9..8e10d17204 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerLongTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerLongTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -16,12 +16,9 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; -import static org.awaitility.Awaitility.await; - import java.util.Set; +import com.rabbitmq.client.ConnectionFactory; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.Test; @@ -36,7 +33,9 @@ import org.springframework.amqp.utils.test.TestUtils; import org.springframework.test.util.ReflectionTestUtils; -import com.rabbitmq.client.ConnectionFactory; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.awaitility.Awaitility.await; /** * @author Gary Russell @@ -65,7 +64,6 @@ public class SimpleMessageListenerContainerLongTests { private final SingleConnectionFactory connectionFactory; - public SimpleMessageListenerContainerLongTests(ConnectionFactory connectionFactory) { this.connectionFactory = new SingleConnectionFactory(connectionFactory); } @@ -101,7 +99,7 @@ private void testChangeConsumerCountGuts(boolean transacted) throws Exception { for (int i = 0; i < 20; i++) { template.convertAndSend(QUEUE, "foo"); } - waitForNConsumers(container, 2); // increased consumers due to work + waitForNConsumers(container, 2); // increased consumers due to work waitForNConsumers(container, 1, 20000); // should stop the extra consumer after 10 seconds idle container.setConcurrentConsumers(3); waitForNConsumers(container, 3); @@ -118,7 +116,8 @@ private void testChangeConsumerCountGuts(boolean transacted) throws Exception { public void testAddQueuesAndStartInCycle() { final SimpleMessageListenerContainer container = new SimpleMessageListenerContainer( this.connectionFactory); - container.setMessageListener(message -> { }); + container.setMessageListener(message -> { + }); container.setConcurrentConsumers(2); container.setReceiveTimeout(10); container.afterPropertiesSet(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java index ed4580e603..6f360ed51e 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -16,29 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.assertj.core.api.Assertions.fail; -import static org.awaitility.Awaitility.await; -import static org.awaitility.Awaitility.with; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.willAnswer; -import static org.mockito.BDDMockito.willReturn; -import static org.mockito.BDDMockito.willThrow; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; - import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; @@ -60,6 +37,11 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Consumer; +import com.rabbitmq.client.Envelope; +import com.rabbitmq.client.PossibleAuthenticationFailureException; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; @@ -95,11 +77,28 @@ import org.springframework.transaction.support.DefaultTransactionStatus; import org.springframework.util.backoff.FixedBackOff; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Consumer; -import com.rabbitmq.client.Envelope; -import com.rabbitmq.client.PossibleAuthenticationFailureException; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.fail; +import static org.awaitility.Awaitility.await; +import static org.awaitility.Awaitility.with; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; /** diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerWithRabbitMQ.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerWithRabbitMQ.java index 1de168c5e4..ad12bcdc8b 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerWithRabbitMQ.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerWithRabbitMQ.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -36,6 +34,8 @@ import org.springframework.amqp.support.converter.SimpleMessageConverter; import org.springframework.beans.DirectFieldAccessor; +import static org.assertj.core.api.Assertions.assertThat; + public final class SimpleMessageListenerWithRabbitMQ { private static Log logger = LogFactory.getLog(SimpleMessageListenerWithRabbitMQ.class); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/StopStartIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/StopStartIntegrationTests.java index 335059437f..2a33ef4cfb 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/StopStartIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/StopStartIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.fail; - import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.AfterAll; @@ -34,6 +32,8 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import static org.assertj.core.api.Assertions.fail; + /** * @author Gary Russell * @author Gunnar Hillert diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/UnackedRawIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/UnackedRawIntegrationTests.java index 9236bd4622..89b4c29a3e 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/UnackedRawIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/UnackedRawIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -16,13 +16,18 @@ package org.springframework.amqp.rabbit.listener; -import static org.assertj.core.api.Assertions.assertThat; - import java.io.IOException; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.DefaultConsumer; +import com.rabbitmq.client.Envelope; +import com.rabbitmq.client.GetResponse; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; @@ -31,13 +36,7 @@ import org.springframework.amqp.rabbit.junit.BrokerTestUtils; import org.springframework.amqp.rabbit.support.Delivery; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.Channel; -import com.rabbitmq.client.Connection; -import com.rabbitmq.client.ConnectionFactory; -import com.rabbitmq.client.DefaultConsumer; -import com.rabbitmq.client.Envelope; -import com.rabbitmq.client.GetResponse; +import static org.assertj.core.api.Assertions.assertThat; /** * Used to verify raw Rabbit Java Client behaviour for corner cases. diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapterTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapterTests.java index 978551e288..ac1d11728e 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapterTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 the original author or authors. + * Copyright 2022-2025 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. @@ -16,14 +16,12 @@ 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 java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Message; @@ -47,7 +45,8 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import com.fasterxml.jackson.databind.ObjectMapper; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandlerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandlerTests.java index f3ef0314cd..25fcee7a00 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandlerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2023-2025 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. @@ -16,9 +16,6 @@ package org.springframework.amqp.rabbit.listener.adapter; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.Mockito.mock; - import java.lang.reflect.Method; import java.lang.reflect.UndeclaredThrowableException; import java.util.ArrayList; @@ -34,6 +31,9 @@ import org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory; import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.mock; + /** * @author Gary Russell * @since 2.4.12 diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapterTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapterTests.java index 0db0b7aaae..58b1c5d942 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapterTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 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. @@ -16,14 +16,6 @@ package org.springframework.amqp.rabbit.listener.adapter; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.willThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - import java.io.IOException; import java.util.HashMap; import java.util.Map; @@ -31,8 +23,10 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import com.rabbitmq.client.Channel; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.Address; @@ -45,8 +39,13 @@ import org.springframework.retry.policy.SimpleRetryPolicy; import org.springframework.retry.support.RetryTemplate; -import com.rabbitmq.client.Channel; -import reactor.core.publisher.Mono; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * @author Dave Syer diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapterTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapterTests.java index f59ae1ce60..aa2d7ab1f2 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapterTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 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. @@ -16,14 +16,6 @@ package org.springframework.amqp.rabbit.listener.adapter; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; @@ -33,6 +25,8 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Channel; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -54,8 +48,13 @@ import org.springframework.messaging.support.MessageBuilder; import org.springframework.util.ReflectionUtils; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.Channel; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * @author Stephane Nicoll diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/AmqpAppenderTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/AmqpAppenderTests.java index c81b48890b..1e0186d313 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/AmqpAppenderTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/AmqpAppenderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 the original author or authors. + * Copyright 2016-2025 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. @@ -16,16 +16,6 @@ package org.springframework.amqp.rabbit.log4j2; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - import java.io.IOException; import java.net.URI; import java.util.Map; @@ -33,6 +23,10 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.DefaultSaslConfig; +import com.rabbitmq.client.JDKSaslConfig; +import com.rabbitmq.client.impl.CRDemoMechanism; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.LoggerContext; @@ -57,10 +51,15 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.test.util.ReflectionTestUtils; -import com.rabbitmq.client.ConnectionFactory; -import com.rabbitmq.client.DefaultSaslConfig; -import com.rabbitmq.client.JDKSaslConfig; -import com.rabbitmq.client.impl.CRDemoMechanism; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; /** * @author Gary Russell @@ -185,24 +184,24 @@ public void testSaslConfig() { Map.class).get("sasl1"); assertThat(RabbitUtils.stringToSaslConfig(TestUtils.getPropertyValue(appender, "manager.saslConfig", String.class), mock(ConnectionFactory.class))) - .isInstanceOf(DefaultSaslConfig.class) - .hasFieldOrPropertyWithValue("mechanism", "PLAIN"); + .isInstanceOf(DefaultSaslConfig.class) + .hasFieldOrPropertyWithValue("mechanism", "PLAIN"); appender = (AmqpAppender) TestUtils.getPropertyValue(logger, "context.configuration.appenders", Map.class).get("sasl2"); assertThat(RabbitUtils.stringToSaslConfig(TestUtils.getPropertyValue(appender, "manager.saslConfig", String.class), mock(ConnectionFactory.class))) - .isInstanceOf(DefaultSaslConfig.class) - .hasFieldOrPropertyWithValue("mechanism", "EXTERNAL"); + .isInstanceOf(DefaultSaslConfig.class) + .hasFieldOrPropertyWithValue("mechanism", "EXTERNAL"); appender = (AmqpAppender) TestUtils.getPropertyValue(logger, "context.configuration.appenders", Map.class).get("sasl3"); assertThat(RabbitUtils.stringToSaslConfig(TestUtils.getPropertyValue(appender, "manager.saslConfig", String.class), mock(ConnectionFactory.class))) - .isInstanceOf(JDKSaslConfig.class); + .isInstanceOf(JDKSaslConfig.class); appender = (AmqpAppender) TestUtils.getPropertyValue(logger, "context.configuration.appenders", Map.class).get("sasl4"); assertThat(RabbitUtils.stringToSaslConfig(TestUtils.getPropertyValue(appender, "manager.saslConfig", String.class), mock(ConnectionFactory.class))) - .isInstanceOf(CRDemoMechanism.CRDemoSaslConfig.class); + .isInstanceOf(CRDemoMechanism.CRDemoSaslConfig.class); } @Test diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/ExtendAmqpAppenderTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/ExtendAmqpAppenderTests.java index 7e8530675d..45b7a6ab4c 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/ExtendAmqpAppenderTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/log4j2/ExtendAmqpAppenderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-2025 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. @@ -16,12 +16,6 @@ package org.springframework.amqp.rabbit.log4j2; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - import java.io.IOException; import java.net.URI; import java.util.Map; @@ -51,6 +45,12 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.test.util.ReflectionTestUtils; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + /** * @author Francesco Scipioni * diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderIntegrationTests.java index e3e3ba7477..52adf6dc58 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 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. @@ -16,17 +16,12 @@ package org.springframework.amqp.rabbit.logback; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.entry; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; +import ch.qos.logback.classic.Logger; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; @@ -46,7 +41,11 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import ch.qos.logback.classic.Logger; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * @author Artem Bilan diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderTests.java index 38bf6f5bfe..ee327ffcae 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/logback/AmqpAppenderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 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. @@ -16,20 +16,13 @@ package org.springframework.amqp.rabbit.logback; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - import java.net.URI; import java.net.URISyntaxException; +import com.rabbitmq.client.DefaultSaslConfig; +import com.rabbitmq.client.JDKSaslConfig; +import com.rabbitmq.client.SaslConfig; +import com.rabbitmq.client.impl.CRDemoMechanism; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -39,10 +32,16 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.test.util.ReflectionTestUtils; -import com.rabbitmq.client.DefaultSaslConfig; -import com.rabbitmq.client.JDKSaslConfig; -import com.rabbitmq.client.SaslConfig; -import com.rabbitmq.client.impl.CRDemoMechanism; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; /** * diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/MissingIdRetryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/MissingIdRetryTests.java index 860a4af405..6c6a63e890 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/MissingIdRetryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/MissingIdRetryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -16,16 +16,6 @@ package org.springframework.amqp.rabbit.retry; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.atLeast; -import static org.mockito.Mockito.atMost; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -61,6 +51,16 @@ import org.springframework.retry.support.RetrySynchronizationManager; import org.springframework.retry.support.RetryTemplate; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.atMost; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + /** * @author Gary Russell * @author Arnaud Cogoluègnes diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererIntegrationTests.java index 14f53fb581..60eb65ce23 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.retry; -import static org.assertj.core.api.Assertions.assertThat; - import java.io.PrintWriter; import java.io.StringWriter; @@ -32,6 +30,8 @@ import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Gary Russell * @since 2.0.5 diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererTests.java index 675c697426..04bdf93009 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2025 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. @@ -16,9 +16,6 @@ package org.springframework.amqp.rabbit.retry; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.verify; - import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.util.Map; @@ -35,6 +32,9 @@ import org.springframework.amqp.core.MessageProperties; import org.springframework.expression.spel.standard.SpelExpressionParser; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; + /** * @author James Carr * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirmsIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirmsIntegrationTests.java index 0619370493..023f4972ef 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirmsIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirmsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 the original author or authors. + * Copyright 2018-2025 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. @@ -16,10 +16,6 @@ package org.springframework.amqp.rabbit.retry; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.fail; - import org.junit.jupiter.api.Test; import org.springframework.amqp.core.AmqpMessageReturnedException; @@ -38,6 +34,10 @@ import org.springframework.expression.common.LiteralExpression; import org.springframework.expression.spel.standard.SpelExpressionParser; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.fail; + /** * @author Gary Russell * @since 2.0.5 diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/ActiveObjectCounterTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/ActiveObjectCounterTests.java index fc779506eb..488a4b5a27 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/ActiveObjectCounterTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/ActiveObjectCounterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -16,14 +16,14 @@ package org.springframework.amqp.rabbit.support; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Dave Syer * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverterTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverterTests.java index 9a76f1de66..3754529715 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverterTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -16,8 +16,6 @@ package org.springframework.amqp.rabbit.support; -import static org.assertj.core.api.Assertions.assertThat; - import java.io.DataInputStream; import java.io.UnsupportedEncodingException; import java.util.Arrays; @@ -25,16 +23,17 @@ import java.util.List; import java.util.Map; +import com.rabbitmq.client.AMQP.BasicProperties; +import com.rabbitmq.client.Envelope; +import com.rabbitmq.client.LongString; +import com.rabbitmq.client.impl.LongStringHelper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.MessageDeliveryMode; import org.springframework.amqp.core.MessageProperties; -import com.rabbitmq.client.AMQP.BasicProperties; -import com.rabbitmq.client.Envelope; -import com.rabbitmq.client.LongString; -import com.rabbitmq.client.impl.LongStringHelper; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Soeren Unruh diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java index 48e981c2f1..828d31892b 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 the original author or authors. + * Copyright 2022-2025 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. @@ -16,12 +16,20 @@ package org.springframework.amqp.rabbit.support.micrometer; -import static org.assertj.core.api.Assertions.assertThat; - import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import io.micrometer.common.KeyValue; +import io.micrometer.common.KeyValues; +import io.micrometer.core.tck.MeterRegistryAssert; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.Span.Kind; +import io.micrometer.tracing.exporter.FinishedSpan; +import io.micrometer.tracing.test.SampleTestRunner; +import io.micrometer.tracing.test.simple.SpanAssert; +import io.micrometer.tracing.test.simple.SpansAssert; + import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.EnableRabbit; import org.springframework.amqp.rabbit.annotation.RabbitListener; @@ -34,15 +42,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import io.micrometer.common.KeyValue; -import io.micrometer.common.KeyValues; -import io.micrometer.core.tck.MeterRegistryAssert; -import io.micrometer.observation.ObservationRegistry; -import io.micrometer.tracing.Span.Kind; -import io.micrometer.tracing.exporter.FinishedSpan; -import io.micrometer.tracing.test.SampleTestRunner; -import io.micrometer.tracing.test.simple.SpanAssert; -import io.micrometer.tracing.test.simple.SpansAssert; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Artem Bilan diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java index 0b6cc28325..fc5ab2aff0 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 the original author or authors. + * Copyright 2022-2025 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. @@ -16,9 +16,6 @@ package org.springframework.amqp.rabbit.support.micrometer; -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; - import java.util.Arrays; import java.util.Deque; import java.util.List; @@ -26,6 +23,23 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import io.micrometer.common.KeyValues; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.core.tck.MeterRegistryAssert; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.tck.TestObservationRegistry; +import io.micrometer.tracing.Span; +import io.micrometer.tracing.TraceContext; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.handler.DefaultTracingObservationHandler; +import io.micrometer.tracing.handler.PropagatingReceiverTracingObservationHandler; +import io.micrometer.tracing.handler.PropagatingSenderTracingObservationHandler; +import io.micrometer.tracing.propagation.Propagator; +import io.micrometer.tracing.test.simple.SimpleSpan; +import io.micrometer.tracing.test.simple.SimpleTracer; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Message; @@ -47,23 +61,8 @@ import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import io.micrometer.common.KeyValues; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; -import io.micrometer.core.instrument.simple.SimpleMeterRegistry; -import io.micrometer.core.tck.MeterRegistryAssert; -import io.micrometer.observation.ObservationHandler; -import io.micrometer.observation.ObservationRegistry; -import io.micrometer.observation.tck.TestObservationRegistry; -import io.micrometer.tracing.Span; -import io.micrometer.tracing.TraceContext; -import io.micrometer.tracing.Tracer; -import io.micrometer.tracing.handler.DefaultTracingObservationHandler; -import io.micrometer.tracing.handler.PropagatingReceiverTracingObservationHandler; -import io.micrometer.tracing.handler.PropagatingSenderTracingObservationHandler; -import io.micrometer.tracing.propagation.Propagator; -import io.micrometer.tracing.test.simple.SimpleSpan; -import io.micrometer.tracing.test.simple.SimpleTracer; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; /** * @author Gary Russell diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/transaction/RabbitExceptionTranslatorTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/transaction/RabbitExceptionTranslatorTests.java index ea67881884..1472a44dcd 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/transaction/RabbitExceptionTranslatorTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/transaction/RabbitExceptionTranslatorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2019 the original author or authors. + * Copyright 2013-2025 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. @@ -16,12 +16,12 @@ package org.springframework.amqp.rabbit.transaction; -import static org.assertj.core.api.Assertions.assertThat; - import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.ConnectException; +import com.rabbitmq.client.PossibleAuthenticationFailureException; +import com.rabbitmq.client.ShutdownSignalException; import org.junit.jupiter.api.Test; import org.springframework.amqp.AmqpAuthenticationException; @@ -32,8 +32,7 @@ import org.springframework.amqp.UncategorizedAmqpException; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; -import com.rabbitmq.client.PossibleAuthenticationFailureException; -import com.rabbitmq.client.ShutdownSignalException; +import static org.assertj.core.api.Assertions.assertThat; /** * @author Sergey Shcherbakov @@ -56,6 +55,7 @@ public void testConvertRabbitAccessException() { assertThat(RabbitExceptionTranslator.convertRabbitAccessException(new UnsupportedEncodingException())).isInstanceOf(AmqpUnsupportedEncodingException.class); assertThat(RabbitExceptionTranslator.convertRabbitAccessException(new Exception() { + private static final long serialVersionUID = 1L; })).isInstanceOf(UncategorizedAmqpException.class); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/transaction/RabbitTransactionManagerIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/transaction/RabbitTransactionManagerIntegrationTests.java index 10ebbc501a..c9cd983542 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/transaction/RabbitTransactionManagerIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/transaction/RabbitTransactionManagerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2019 the original author or authors. + * Copyright 2011-2025 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. @@ -16,9 +16,6 @@ package org.springframework.amqp.rabbit.transaction; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -29,6 +26,9 @@ import org.springframework.beans.factory.DisposableBean; import org.springframework.transaction.support.TransactionTemplate; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + /** * @author David Syer * @author Gunnar Hillert @@ -135,9 +135,11 @@ public void testSendInTransactionWithRollback() throws Exception { @SuppressWarnings("serial") private class PlannedException extends RuntimeException { + PlannedException() { super("Planned"); } + } } diff --git a/src/checkstyle/checkstyle.xml b/src/checkstyle/checkstyle.xml index 57b294ffdc..f3f6493927 100644 --- a/src/checkstyle/checkstyle.xml +++ b/src/checkstyle/checkstyle.xml @@ -87,10 +87,10 @@ - + - + diff --git a/src/eclipse/org.eclipse.core.resources.prefs b/src/eclipse/org.eclipse.core.resources.prefs new file mode 100644 index 0000000000..99f26c0203 --- /dev/null +++ b/src/eclipse/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/src/eclipse/org.eclipse.jdt.core.prefs b/src/eclipse/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000000..3b3666c96c --- /dev/null +++ b/src/eclipse/org.eclipse.jdt.core.prefs @@ -0,0 +1,469 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/main/resources=UTF-8 +encoding//src/test/java=UTF-8 +encoding//src/test/resources=UTF-8 +org.eclipse.jdt.core.codeComplete.argumentPrefixes= +org.eclipse.jdt.core.codeComplete.argumentSuffixes= +org.eclipse.jdt.core.codeComplete.fieldPrefixes= +org.eclipse.jdt.core.codeComplete.fieldSuffixes= +org.eclipse.jdt.core.codeComplete.localPrefixes= +org.eclipse.jdt.core.codeComplete.localSuffixes= +org.eclipse.jdt.core.codeComplete.staticFieldPrefixes= +org.eclipse.jdt.core.codeComplete.staticFieldSuffixes= +org.eclipse.jdt.core.codeComplete.staticFinalFieldPrefixes= +org.eclipse.jdt.core.codeComplete.staticFinalFieldSuffixes= +org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=enabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore +org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=info +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=ignore +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=ignore +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error +org.eclipse.jdt.core.compiler.problem.nullReference=ignore +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore +org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.suppressWarningsNotFullyAnalysed=ignore +org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=ignore +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning +org.eclipse.jdt.core.compiler.source=17 +org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false +org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647 +org.eclipse.jdt.core.formatter.align_type_members_on_columns=false +org.eclipse.jdt.core.formatter.align_variable_declarations_on_columns=false +org.eclipse.jdt.core.formatter.align_with_spaces=false +org.eclipse.jdt.core.formatter.alignment_for_additive_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_assignment=0 +org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_bitwise_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_compact_if=16 +org.eclipse.jdt.core.formatter.alignment_for_compact_loops=16 +org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_conditional_expression_chain=0 +org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_for_loop_header=0 +org.eclipse.jdt.core.formatter.alignment_for_logical_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0 +org.eclipse.jdt.core.formatter.alignment_for_module_statements=16 +org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 +org.eclipse.jdt.core.formatter.alignment_for_multiplicative_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_parameterized_type_references=0 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_relational_operator=0 +org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80 +org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16 +org.eclipse.jdt.core.formatter.alignment_for_shift_operator=0 +org.eclipse.jdt.core.formatter.alignment_for_string_concatenation=16 +org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_type_arguments=0 +org.eclipse.jdt.core.formatter.alignment_for_type_parameters=0 +org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 +org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 +org.eclipse.jdt.core.formatter.blank_lines_after_package=1 +org.eclipse.jdt.core.formatter.blank_lines_before_field=0 +org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0 +org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 +org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 +org.eclipse.jdt.core.formatter.blank_lines_before_method=1 +org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1 +org.eclipse.jdt.core.formatter.blank_lines_before_package=0 +org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1 +org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1 +org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.comment.align_tags_descriptions_grouped=false +org.eclipse.jdt.core.formatter.comment.align_tags_names_descriptions=false +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false +org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position=false +org.eclipse.jdt.core.formatter.comment.format_block_comments=true +org.eclipse.jdt.core.formatter.comment.format_header=false +org.eclipse.jdt.core.formatter.comment.format_html=true +org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true +org.eclipse.jdt.core.formatter.comment.format_line_comments=true +org.eclipse.jdt.core.formatter.comment.format_source_code=false +org.eclipse.jdt.core.formatter.comment.indent_parameter_description=false +org.eclipse.jdt.core.formatter.comment.indent_root_tags=false +org.eclipse.jdt.core.formatter.comment.indent_tag_description=false +org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=do not insert +org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert +org.eclipse.jdt.core.formatter.comment.line_length=80 +org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true +org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true +org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false +org.eclipse.jdt.core.formatter.compact_else_if=true +org.eclipse.jdt.core.formatter.continuation_indentation=2 +org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2 +org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off +org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on +org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false +org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true +org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true +org.eclipse.jdt.core.formatter.indent_empty_lines=false +org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true +org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=true +org.eclipse.jdt.core.formatter.indentation.size=4 +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert +org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_after_additive_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_bitwise_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert +org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow=insert +org.eclipse.jdt.core.formatter.insert_space_after_logical_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_multiplicative_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_relational_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert +org.eclipse.jdt.core.formatter.insert_space_after_shift_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_string_concatenation=insert +org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_additive_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_bitwise_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow=insert +org.eclipse.jdt.core.formatter.insert_space_before_logical_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_multiplicative_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert +org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_relational_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_shift_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_string_concatenation=insert +org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.join_lines_in_comments=true +org.eclipse.jdt.core.formatter.join_wrapped_lines=true +org.eclipse.jdt.core.formatter.keep_annotation_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_anonymous_type_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_code_block_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false +org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false +org.eclipse.jdt.core.formatter.keep_enum_constant_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_enum_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_if_then_body_block_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false +org.eclipse.jdt.core.formatter.keep_lambda_body_block_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_loop_body_block_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_method_body_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_simple_do_while_body_on_same_line=false +org.eclipse.jdt.core.formatter.keep_simple_for_body_on_same_line=false +org.eclipse.jdt.core.formatter.keep_simple_getter_setter_on_one_line=false +org.eclipse.jdt.core.formatter.keep_simple_while_body_on_same_line=false +org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false +org.eclipse.jdt.core.formatter.keep_type_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.lineSplit=90 +org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false +org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 +org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 +org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_catch_clause=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_enum_constant_declaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_for_statment=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_if_while_statement=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_lambda_declaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_method_delcaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_method_invocation=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_switch_statement=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_try_clause=common_lines +org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true +org.eclipse.jdt.core.formatter.tabulation.char=tab +org.eclipse.jdt.core.formatter.tabulation.size=4 +org.eclipse.jdt.core.formatter.use_on_off_tags=true +org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false +org.eclipse.jdt.core.formatter.wrap_before_additive_operator=true +org.eclipse.jdt.core.formatter.wrap_before_assignment_operator=false +org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true +org.eclipse.jdt.core.formatter.wrap_before_bitwise_operator=true +org.eclipse.jdt.core.formatter.wrap_before_conditional_operator=true +org.eclipse.jdt.core.formatter.wrap_before_logical_operator=true +org.eclipse.jdt.core.formatter.wrap_before_multiplicative_operator=true +org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true +org.eclipse.jdt.core.formatter.wrap_before_relational_operator=true +org.eclipse.jdt.core.formatter.wrap_before_shift_operator=true +org.eclipse.jdt.core.formatter.wrap_before_string_concatenation=true +org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true diff --git a/src/eclipse/org.eclipse.jdt.ui.prefs b/src/eclipse/org.eclipse.jdt.ui.prefs new file mode 100644 index 0000000000..1961fa9533 --- /dev/null +++ b/src/eclipse/org.eclipse.jdt.ui.prefs @@ -0,0 +1,66 @@ +cleanup.add_default_serial_version_id=true +cleanup.add_generated_serial_version_id=false +cleanup.add_missing_annotations=false +cleanup.add_missing_deprecated_annotations=true +cleanup.add_missing_methods=false +cleanup.add_missing_nls_tags=false +cleanup.add_missing_override_annotations=true +cleanup.add_missing_override_annotations_interface_methods=true +cleanup.add_serial_version_id=false +cleanup.always_use_blocks=true +cleanup.always_use_parentheses_in_expressions=false +cleanup.always_use_this_for_non_static_field_access=true +cleanup.always_use_this_for_non_static_method_access=false +cleanup.convert_to_enhanced_for_loop=false +cleanup.correct_indentation=false +cleanup.format_source_code=false +cleanup.format_source_code_changes_only=false +cleanup.make_local_variable_final=false +cleanup.make_parameters_final=false +cleanup.make_private_fields_final=true +cleanup.make_type_abstract_if_missing_method=false +cleanup.make_variable_declarations_final=true +cleanup.never_use_blocks=false +cleanup.never_use_parentheses_in_expressions=true +cleanup.organize_imports=false +cleanup.qualify_static_field_accesses_with_declaring_class=false +cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true +cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true +cleanup.qualify_static_member_accesses_with_declaring_class=true +cleanup.qualify_static_method_accesses_with_declaring_class=false +cleanup.remove_private_constructors=true +cleanup.remove_trailing_whitespaces=true +cleanup.remove_trailing_whitespaces_all=true +cleanup.remove_trailing_whitespaces_ignore_empty=false +cleanup.remove_unnecessary_casts=true +cleanup.remove_unnecessary_nls_tags=true +cleanup.remove_unused_imports=true +cleanup.remove_unused_local_variables=false +cleanup.remove_unused_private_fields=true +cleanup.remove_unused_private_members=false +cleanup.remove_unused_private_methods=true +cleanup.remove_unused_private_types=true +cleanup.sort_members=false +cleanup.sort_members_all=false +cleanup.use_blocks=true +cleanup.use_blocks_only_for_return_and_throw=false +cleanup.use_parentheses_in_expressions=false +cleanup.use_this_for_non_static_field_access=true +cleanup.use_this_for_non_static_field_access_only_if_necessary=false +cleanup.use_this_for_non_static_method_access=false +cleanup.use_this_for_non_static_method_access_only_if_necessary=true +cleanup_profile=_Spring +cleanup_settings_version=2 +eclipse.preferences.version=1 +formatter_profile=_spring +formatter_settings_version=13 +org.eclipse.jdt.ui.exception.name=e +org.eclipse.jdt.ui.gettersetter.use.is=true +org.eclipse.jdt.ui.ignorelowercasenames=true +org.eclipse.jdt.ui.importorder=java;javax;;org.springframework;\#; +org.eclipse.jdt.ui.javadoc=true +org.eclipse.jdt.ui.keywordthis=false +org.eclipse.jdt.ui.ondemandthreshold=9999 +org.eclipse.jdt.ui.overrideannotation=true +org.eclipse.jdt.ui.staticondemandthreshold=9999 +org.eclipse.jdt.ui.text.custom_code_templates= diff --git a/src/idea/spring-framework.xml b/src/idea/spring-framework.xml new file mode 100644 index 0000000000..a66a6d14c3 --- /dev/null +++ b/src/idea/spring-framework.xml @@ -0,0 +1,269 @@ + + + + + \ No newline at end of file From a58c73c83856c89d3408051e05e88179b6c00e90 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 28 Jan 2025 17:18:38 -0500 Subject: [PATCH 667/737] Start version `4.0.0` * Upgrade to Gradle `8.12.1` * Upgrade to Spring Framework `7.0.0` * Upgrade to Spring Data `2025.1.0` * Upgrade to Kotlin `2.1.10` * Upgrade to RabbitMQ Client `5.24.0` * Upgrade to RabbitMQ Stream Client `0.22.0` * Other minor upgrades --- .github/dependabot.yml | 43 ++++++++++++++ build.gradle | 73 ++++++++++-------------- gradle.properties | 2 +- gradle/publish-maven.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 +- settings.gradle | 11 ++-- 6 files changed, 83 insertions(+), 52 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3db6fde556..249a97f0c4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -30,6 +30,49 @@ updates: - org.awaitility:awaitility - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: saturday + labels: + - 'type: task' + groups: + development-dependencies: + patterns: + - '*' + + - package-ecosystem: gradle + target-branch: 3.2.x + directory: / + schedule: + interval: weekly + day: saturday + ignore: + - dependency-name: '*' + update-types: + - version-update:semver-major + - version-update:semver-minor + open-pull-requests-limit: 10 + labels: + - 'type: dependency-upgrade' + groups: + development-dependencies: + update-types: + - patch + patterns: + - com.gradle.* + - com.github.spotbugs + - io.spring.* + - org.ajoberstar.grgit + - org.antora + - io.micrometer:micrometer-docs-generator + - com.willowtreeapps.assertk:assertk-jvm + - org.hibernate.validator:hibernate-validator + - org.apache.httpcomponents.client5:httpclient5 + - org.awaitility:awaitility + + - package-ecosystem: github-actions + target-branch: 3.2.x directory: / schedule: interval: weekly diff --git a/build.gradle b/build.gradle index 35c9d41acf..a5a5be87f7 100644 --- a/build.gradle +++ b/build.gradle @@ -1,13 +1,9 @@ buildscript { - ext.kotlinVersion = '1.9.25' + ext.kotlinVersion = '2.1.10' ext.isCI = System.getenv('GITHUB_ACTION') repositories { mavenCentral() gradlePluginPortal() - maven { url 'https://repo.spring.io/plugins-release-local' } - if (version.endsWith('SNAPSHOT')) { - maven { url 'https://repo.spring.io/snapshot' } - } } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" @@ -23,8 +19,8 @@ plugins { id 'io.spring.dependency-management' version '1.1.7' apply false id 'org.antora' version '1.0.0' id 'io.spring.antora.generate-antora-yml' version '0.0.1' - id 'com.github.spotbugs' version '6.0.28' - id 'io.freefair.aggregate-javadoc' version '8.10.2' + id 'com.github.spotbugs' version '6.1.2' + id 'io.freefair.aggregate-javadoc' version '8.11' } description = 'Spring AMQP' @@ -45,30 +41,30 @@ ext { } modifiedFiles.finalizeValueOnRead() - assertjVersion = '3.26.3' + assertjVersion = '3.27.3' assertkVersion = '0.28.1' awaitilityVersion = '4.2.2' commonsHttpClientVersion = '5.4.1' - commonsPoolVersion = '2.12.0' + commonsPoolVersion = '2.12.1' hamcrestVersion = '3.0' hibernateValidationVersion = '8.0.2.Final' jacksonBomVersion = '2.18.2' jaywayJsonPathVersion = '2.9.0' junit4Version = '4.13.2' junitJupiterVersion = '5.11.4' - kotlinCoroutinesVersion = '1.8.1' + kotlinCoroutinesVersion = '1.10.1' log4jVersion = '2.24.3' logbackVersion = '1.5.16' micrometerDocsVersion = '1.0.4' - micrometerVersion = '1.14.3' - micrometerTracingVersion = '1.4.2' - mockitoVersion = '5.14.2' - rabbitmqStreamVersion = '0.18.0' - rabbitmqVersion = '5.22.0' + micrometerVersion = '1.15.0-SNAPSHOT' + micrometerTracingVersion = '1.5.0-SNAPSHOT' + mockitoVersion = '5.15.2' + rabbitmqStreamVersion = '0.22.0' + rabbitmqVersion = '5.24.0' reactorVersion = '2024.0.2' - springDataVersion = '2024.1.2' + springDataVersion = '2025.1.0-SNAPSHOT' springRetryVersion = '2.0.11' - springVersion = '6.2.2' + springVersion = '7.0.0-SNAPSHOT' testcontainersVersion = '1.20.4' javaProjects = subprojects - project(':spring-amqp-bom') @@ -150,7 +146,7 @@ ext { expandPlaceholders = '**/quick-tour.xml' javadocLinks = [ 'https://docs.oracle.com/en/java/javase/17/docs/api/', - 'https://jakarta.ee/specifications/platform/9/apidocs/', + 'https://jakarta.ee/specifications/platform/11/apidocs/', 'https://docs.spring.io/spring-framework/docs/current/javadoc-api/' ] as String[] } @@ -213,18 +209,13 @@ configure(javaProjects) { subproject -> exclude group: 'org.hamcrest' } - // To avoid compiler warnings about @API annotations in JUnit code - testCompileOnly 'org.apiguardian:apiguardian-api:1.0.0' - testImplementation 'org.jetbrains.kotlin:kotlin-reflect' testImplementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' - - } // enable all compiler warnings; individual projects may customize further ext.xLintArg = '-Xlint:all,-options,-processing,-deprecation' - [compileJava, compileTestJava]*.options*.compilerArgs = [xLintArg] + [compileJava, compileTestJava]*.options*.compilerArgs = [xLintArg, '-parameters'] publishing { publications { @@ -310,8 +301,8 @@ configure(javaProjects) { subproject -> } checkstyle { - configDirectory.set(rootProject.file("src/checkstyle")) - toolVersion = '10.18.2' + configDirectory.set(rootProject.file('src/checkstyle')) + toolVersion = '10.21.1' } jar { @@ -339,7 +330,6 @@ configure(javaProjects) { subproject -> } check.dependsOn javadoc - } project('spring-amqp') { @@ -361,14 +351,9 @@ project('spring-amqp') { optionalApi 'com.fasterxml.jackson.module:jackson-module-parameter-names' optionalApi 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' optionalApi 'com.fasterxml.jackson.datatype:jackson-datatype-joda' - optionalApi('com.fasterxml.jackson.module:jackson-module-kotlin') { - exclude group: 'org.jetbrains.kotlin' - } + optionalApi 'com.fasterxml.jackson.module:jackson-module-kotlin' // Spring Data projection message binding support - optionalApi('org.springframework.data:spring-data-commons') { - exclude group: 'org.springframework' - exclude group: 'io.micrometer' - } + optionalApi 'org.springframework.data:spring-data-commons' optionalApi "com.jayway.jsonpath:json-path:$jaywayJsonPathVersion" testImplementation "org.assertj:assertj-core:$assertjVersion" @@ -409,6 +394,7 @@ project('spring-rabbit') { api 'org.springframework:spring-messaging' api 'org.springframework:spring-tx' api 'io.micrometer:micrometer-observation' + optionalApi 'org.springframework:spring-aop' optionalApi 'org.springframework:spring-webflux' optionalApi "org.apache.httpcomponents.client5:httpclient5:$commonsHttpClientVersion" @@ -419,23 +405,23 @@ project('spring-rabbit') { optionalApi 'io.micrometer:micrometer-core' optionalApi 'io.micrometer:micrometer-tracing' // Spring Data projection message binding support - optionalApi("org.springframework.data:spring-data-commons") { - exclude group: 'org.springframework' - } + optionalApi 'org.springframework.data:spring-data-commons' optionalApi "com.jayway.jsonpath:json-path:$jaywayJsonPathVersion" optionalApi "org.apache.commons:commons-pool2:$commonsPoolVersion" optionalApi "org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlinCoroutinesVersion" testApi project(':spring-rabbit-junit') + testImplementation("com.willowtreeapps.assertk:assertk-jvm:$assertkVersion") testImplementation "org.hibernate.validator:hibernate-validator:$hibernateValidationVersion" testImplementation 'io.micrometer:micrometer-observation-test' testImplementation 'io.micrometer:micrometer-tracing-bridge-brave' testImplementation 'io.micrometer:micrometer-tracing-test' testImplementation 'io.micrometer:micrometer-tracing-integration-test' - testImplementation "org.testcontainers:rabbitmq" + testImplementation 'org.testcontainers:rabbitmq' testImplementation 'org.testcontainers:junit-jupiter' testImplementation "org.apache.httpcomponents.client5:httpclient5:$commonsHttpClientVersion" + testRuntimeOnly 'com.fasterxml.jackson.core:jackson-core' testRuntimeOnly 'com.fasterxml.jackson.core:jackson-databind' testRuntimeOnly 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' @@ -461,13 +447,14 @@ project('spring-rabbit-stream') { optionalApi 'io.micrometer:micrometer-core' testApi project(':spring-rabbit-junit') + testRuntimeOnly 'com.fasterxml.jackson.core:jackson-core' testRuntimeOnly 'com.fasterxml.jackson.core:jackson-databind' testRuntimeOnly 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' testRuntimeOnly 'com.fasterxml.jackson.module:jackson-module-kotlin' - testImplementation "org.testcontainers:rabbitmq" - testImplementation "org.testcontainers:junit-jupiter" + testImplementation 'org.testcontainers:rabbitmq' + testImplementation 'org.testcontainers:junit-jupiter' testImplementation 'org.apache.logging.log4j:log4j-slf4j-impl' testImplementation 'org.springframework:spring-webflux' testImplementation 'io.micrometer:micrometer-observation-test' @@ -487,11 +474,12 @@ project('spring-rabbit-junit') { api 'org.springframework:spring-web' api 'org.junit.jupiter:junit-jupiter-api' api "org.assertj:assertj-core:$assertjVersion" + optionalApi("junit:junit:$junit4Version") { exclude group: 'org.hamcrest', module: 'hamcrest-core' } - optionalApi "org.testcontainers:rabbitmq" - optionalApi "org.testcontainers:junit-jupiter" + optionalApi 'org.testcontainers:rabbitmq' + optionalApi 'org.testcontainers:junit-jupiter' optionalApi "ch.qos.logback:logback-classic:$logbackVersion" optionalApi 'org.apache.logging.log4j:log4j-core' compileOnly 'org.apiguardian:apiguardian-api:1.0.0' @@ -507,6 +495,7 @@ project('spring-rabbit-test') { api "org.hamcrest:hamcrest-library:$hamcrestVersion" api "org.hamcrest:hamcrest-core:$hamcrestVersion" api "org.mockito:mockito-core:$mockitoVersion" + testImplementation project(':spring-rabbit').sourceSets.test.output } } diff --git a/gradle.properties b/gradle.properties index 6d61f0a56d..0e863135dd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=3.2.3-SNAPSHOT +version=4.0.0-SNAPSHOT org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true diff --git a/gradle/publish-maven.gradle b/gradle/publish-maven.gradle index 0b72dba7c3..e3a05676e0 100644 --- a/gradle/publish-maven.gradle +++ b/gradle/publish-maven.gradle @@ -53,7 +53,7 @@ publishing { developer { id = 'markfisher' name = 'Mark Fisher' - email = 'mark.fisher@broadcom.com' + email = 'mark.ryan.fisher@gmail.com' roles = ['project founder'] } developer { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e1b837a19c..d71047787f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=7a00d51fb93147819aab76024feece20b6b84e420694101f276be952e08bef03 -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +distributionSha256Sum=8d97a97984f6cbd2b85fe4c60a743440a347544bf18818048e611f5288d46c94 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/settings.gradle b/settings.gradle index 083f33a8a1..f67d410c94 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,9 +11,8 @@ plugins { rootProject.name = 'spring-amqp-dist' -include 'spring-amqp' -include 'spring-amqp-bom' -include 'spring-rabbit' -include 'spring-rabbit-stream' -include 'spring-rabbit-junit' -include 'spring-rabbit-test' +rootDir.eachDir { dir -> + if (dir.name.startsWith('spring-')) { + include ":${dir.name}" + } +} \ No newline at end of file From 5646da385c59d318243a4772ef413cbe626b75d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 22:22:36 +0000 Subject: [PATCH 668/737] Bump com.github.spotbugs in the development-dependencies group Bumps the development-dependencies group with 1 update: com.github.spotbugs. Updates `com.github.spotbugs` from 6.1.2 to 6.1.3 --- updated-dependencies: - dependency-name: com.github.spotbugs dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a5a5be87f7..0522432d42 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ plugins { id 'io.spring.dependency-management' version '1.1.7' apply false id 'org.antora' version '1.0.0' id 'io.spring.antora.generate-antora-yml' version '0.0.1' - id 'com.github.spotbugs' version '6.1.2' + id 'com.github.spotbugs' version '6.1.3' id 'io.freefair.aggregate-javadoc' version '8.11' } From f621b86b7825f5548b45215696e7bb6bf910b057 Mon Sep 17 00:00:00 2001 From: Johan Kaving Date: Tue, 4 Feb 2025 22:54:46 +0100 Subject: [PATCH 669/737] GH-2949: Fix retryCount handling in the `DefaultMessagePropertiesConverter` Fixes: #2949 Issue link: https://github.com/spring-projects/spring-amqp/issues/2949 * The handling of `retryCount` is moved into `convertHeadersIfNecessary()`, so that we can still return a `Collections.emptyMap()` if there are no headers in the source `MessageProperties` and no `retryCount` to add. Signed-off-by: Johan Kaving [artem.bilan@broadcom.com improve commit message] **Auto-cherry-pick to `3.2.x`** Signed-off-by: Artem Bilan --- .../DefaultMessagePropertiesConverter.java | 17 ++++++++++------- .../DefaultMessagePropertiesConverterTests.java | 12 ++++++++++++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java index 2a9bf33f71..f9f4d9cf40 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java @@ -43,6 +43,7 @@ * @author Raylax Grey * @author Artem Bilan * @author Ngoc Nhan + * @author Johan Kaving * * @since 1.0 */ @@ -155,11 +156,7 @@ else if (MessageProperties.RETRY_COUNT.equals(key)) { @Override public BasicProperties fromMessageProperties(final MessageProperties source, final String charset) { BasicProperties.Builder target = new BasicProperties.Builder(); - Map headers = convertHeadersIfNecessary(source.getHeaders()); - long retryCount = source.getRetryCount(); - if (retryCount > 0) { - headers.put(MessageProperties.RETRY_COUNT, retryCount); - } + Map headers = convertHeadersIfNecessary(source); target.headers(headers) .timestamp(source.getTimestamp()) .messageId(source.getMessageId()) @@ -186,14 +183,20 @@ public BasicProperties fromMessageProperties(final MessageProperties source, fin return target.build(); } - private Map convertHeadersIfNecessary(Map headers) { - if (CollectionUtils.isEmpty(headers)) { + private Map convertHeadersIfNecessary(MessageProperties source) { + Map headers = source.getHeaders(); + long retryCount = source.getRetryCount(); + + if (CollectionUtils.isEmpty(headers) && retryCount == 0) { return Collections.emptyMap(); } Map writableHeaders = new HashMap<>(); for (Map.Entry entry : headers.entrySet()) { writableHeaders.put(entry.getKey(), this.convertHeaderValueIfNecessary(entry.getValue())); } + if (retryCount > 0) { + writableHeaders.put(MessageProperties.RETRY_COUNT, retryCount); + } return writableHeaders; } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverterTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverterTests.java index 3754529715..91ce11892f 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverterTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverterTests.java @@ -38,6 +38,7 @@ /** * @author Soeren Unruh * @author Gary Russell + * @author Johan Kaving * @since 1.3 */ public class DefaultMessagePropertiesConverterTests { @@ -200,6 +201,17 @@ public void testClassHeader() { assertThat(basic.getHeaders().get("aClass")).isEqualTo(getClass().getName()); } + @Test + public void testRetryCount() { + MessageProperties props = new MessageProperties(); + props.incrementRetryCount(); + BasicProperties basic = new DefaultMessagePropertiesConverter().fromMessageProperties(props, "UTF8"); + assertThat(basic.getHeaders().get(MessageProperties.RETRY_COUNT)).isEqualTo(1L); + props.incrementRetryCount(); + basic = new DefaultMessagePropertiesConverter().fromMessageProperties(props, "UTF8"); + assertThat(basic.getHeaders().get(MessageProperties.RETRY_COUNT)).isEqualTo(2L); + } + private static class Foo { Foo() { From cc98ae369a0e9519f051e78eaf2454d7ba52b1a6 Mon Sep 17 00:00:00 2001 From: Johan Kaving Date: Wed, 5 Feb 2025 16:50:47 +0100 Subject: [PATCH 670/737] Fix README on publishing to local Maven Since the switch to the "maven-publish" plugin (instead of the deprecated "maven" plugin) in 48e816d0a901893ed50a1fcd0c74009d3d0bee8c, the correct way to publish to a local Maven repository is to use the "publishToMavenLocal" task. Signed-off-by: Johan Kaving --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cbd858118c..078ba7ef9e 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,9 @@ If you encounter out of memory errors during the build, increase available heap GRADLE_OPTS='-XX:MaxPermSize=1024m -Xmx1024m' -To build and install jars into your local Maven cache: +To build and publish jars to your local Maven repository: - ./gradlew install + ./gradlew publishToMavenLocal To build api Javadoc (results will be in `build/api`): From d84f727bde9d95304a84253cb0db2d2c7de780f2 Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Wed, 5 Feb 2025 22:53:54 +0700 Subject: [PATCH 671/737] Remove deprecated `ListenerLowCardinalityTags.DELIVERY_TAG` Fixes: https://github.com/spring-projects/spring-amqp/issues/2921 --- .../micrometer/RabbitListenerObservation.java | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java index 19e75baf0b..d575a07b70 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java @@ -28,6 +28,7 @@ * @author Gary Russell * @author Vincent Meunier * @author Artem Bilan + * @author Ngoc Nhan * * @since 3.0 */ @@ -84,24 +85,6 @@ public String asString() { return "messaging.destination.name"; } - }, - - /** - * The delivery tag. - * After deprecation this key is not exposed as a low cardinality tag. - * - * @since 3.2 - * - * @deprecated in favor of {@link ListenerHighCardinalityTags#DELIVERY_TAG} - */ - @Deprecated(since = "3.2.1", forRemoval = true) - DELIVERY_TAG { - - @Override - public String asString() { - return "messaging.rabbitmq.message.delivery_tag"; - } - } } From 0173186675f0567099c337d3366c2c9ca7f9ef45 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 5 Feb 2025 12:58:13 -0500 Subject: [PATCH 672/737] GH-2953: Add NullAway support into the project Fixes: https://github.com/spring-projects/spring-amqp/issues/2953 * Migrate nullability to JSpecify * Add `net.ltgt.errorprone` Gradle plugin and respective `NullAway` configuration * Remove redundant `com.github.spotbugs` since it covered now by the `net.ltgt.errorprone` tool * Use Java `23` for the latest fixes on Java nullability * Fix JavaDocs and `this-escape` warnings in the code * Use `main` for reusable workflows which come already with Java `23` * Fix all the nullability problems in the project --- .github/dependabot.yml | 4 +- .github/workflows/pr-build.yml | 2 +- .github/workflows/release.yml | 4 +- build.gradle | 54 +-- .../springframework/amqp/AmqpException.java | 9 +- .../AmqpRejectAndDontRequeueException.java | 11 +- .../amqp/UncategorizedAmqpException.java | 6 +- .../amqp/core/AbstractBuilder.java | 12 +- .../amqp/core/AbstractDeclarable.java | 12 +- .../amqp/core/AbstractExchange.java | 7 +- .../springframework/amqp/core/AmqpAdmin.java | 5 +- .../core/AmqpMessageReturnedException.java | 7 +- .../amqp/core/AmqpTemplate.java | 40 +- .../amqp/core/AnonymousQueue.java | 22 +- .../amqp/core/AsyncAmqpTemplate.java | 10 +- .../amqp/core/BaseExchangeBuilder.java | 32 +- .../springframework/amqp/core/Binding.java | 29 +- .../amqp/core/BindingBuilder.java | 46 ++- .../amqp/core/ConsistentHashExchange.java | 11 +- .../amqp/core/CustomExchange.java | 11 +- .../springframework/amqp/core/Declarable.java | 11 +- .../amqp/core/DirectExchange.java | 11 +- .../springframework/amqp/core/Exchange.java | 5 +- .../amqp/core/FanoutExchange.java | 11 +- .../amqp/core/HeadersExchange.java | 9 +- .../amqp/core/MessageBuilderSupport.java | 6 +- .../amqp/core/MessageDeliveryMode.java | 30 +- .../amqp/core/MessagePostProcessor.java | 10 +- .../amqp/core/MessageProperties.java | 166 ++++---- .../org/springframework/amqp/core/Queue.java | 8 +- .../amqp/core/QueueBuilder.java | 5 +- .../amqp/core/ReceiveAndReplyCallback.java | 7 +- .../amqp/core/TopicExchange.java | 10 +- .../amqp/core/package-info.java | 1 + .../amqp/event/package-info.java | 1 + .../springframework/amqp/package-info.java | 1 + .../support/AmqpMessageHeaderAccessor.java | 54 +-- .../support/ConditionalExceptionLogger.java | 5 +- .../support/SendRetryContextAccessor.java | 8 +- .../amqp/support/SimpleAmqpHeaderMapper.java | 16 +- .../AbstractJackson2MessageConverter.java | 59 ++- .../converter/AbstractJavaTypeMapper.java | 12 +- .../converter/AbstractMessageConverter.java | 5 +- ...ContentTypeDelegatingMessageConverter.java | 9 +- .../support/converter/DefaultClassMapper.java | 23 +- .../DefaultJackson2JavaTypeMapper.java | 14 +- .../converter/Jackson2JavaTypeMapper.java | 2 +- .../MarshallingMessageConverter.java | 17 +- .../support/converter/MessageConverter.java | 7 +- .../converter/MessagingMessageConverter.java | 5 +- .../converter/RemoteInvocationResult.java | 12 +- .../converter/RemoteInvocationUtils.java | 6 +- .../converter/SerializerMessageConverter.java | 22 +- .../converter/SimpleMessageConverter.java | 48 ++- .../converter/SmartMessageConverter.java | 6 +- .../amqp/support/converter/package-info.java | 2 +- .../amqp/support/package-info.java | 1 + .../AbstractCompressingPostProcessor.java | 6 +- .../AbstractDecompressingPostProcessor.java | 12 +- .../postprocessor/DeflaterPostProcessor.java | 6 +- .../postprocessor/InflaterPostProcessor.java | 7 +- .../MessagePostProcessorUtils.java | 7 +- .../support/postprocessor/package-info.java | 1 + .../springframework/amqp/utils/JavaUtils.java | 18 +- .../amqp/utils/SerializationUtils.java | 10 +- .../amqp/utils/package-info.java | 1 + .../amqp/utils/test/TestUtils.java | 8 +- .../amqp/utils/test/package-info.java | 5 + ...ntTypeDelegatingMessageConverterTests.java | 23 +- .../Jackson2JsonMessageConverterTests.java | 4 +- .../MessagingMessageConverterTests.java | 5 - .../junit/AbstractTestContainerTests.java | 20 +- .../rabbit/junit/BrokerRunningSupport.java | 72 ++-- .../amqp/rabbit/junit/JUnitUtils.java | 9 +- .../amqp/rabbit/junit/package-info.java | 1 + .../StreamRabbitListenerContainerFactory.java | 15 +- .../rabbit/stream/config/SuperStream.java | 25 +- .../stream/config/SuperStreamBuilder.java | 12 +- .../rabbit/stream/config/package-info.java | 2 +- .../listener/StreamListenerContainer.java | 48 ++- .../adapter/StreamMessageListenerAdapter.java | 13 +- .../stream/listener/adapter/package-info.java | 2 +- .../rabbit/stream/listener/package-info.java | 2 +- .../RabbitStreamMessageReceiverContext.java | 4 +- .../RabbitStreamMessageSenderContext.java | 3 +- .../stream/micrometer/package-info.java | 3 +- .../producer/RabbitStreamOperations.java | 2 +- .../stream/producer/RabbitStreamTemplate.java | 53 ++- .../stream/producer/StreamSendException.java | 5 +- .../rabbit/stream/producer/package-info.java | 2 +- .../rabbit/stream/retry/package-info.java | 1 + .../support/StreamMessageProperties.java | 27 +- .../DefaultStreamMessageConverter.java | 7 +- .../support/converter/package-info.java | 2 +- .../rabbit/stream/support/package-info.java | 2 +- .../stream/listener/RabbitListenerTests.java | 9 +- .../test/RabbitListenerTestHarness.java | 35 +- .../amqp/rabbit/test/TestRabbitTemplate.java | 24 +- .../SpringRabbitContextCustomizerFactory.java | 6 +- .../rabbit/test/context/package-info.java | 1 + .../rabbit/test/mockito/LambdaAnswer.java | 7 +- ...LatchCountDownAndCallRealMethodAnswer.java | 11 +- .../rabbit/test/mockito/package-info.java | 1 + .../amqp/rabbit/test/package-info.java | 1 + .../amqp/rabbit/AsyncRabbitTemplate.java | 161 ++++---- .../amqp/rabbit/RabbitConverterFuture.java | 14 +- .../amqp/rabbit/RabbitFuture.java | 25 +- .../amqp/rabbit/RabbitMessageFuture.java | 9 +- .../amqp/rabbit/TimeoutTask.java | 7 +- .../MultiRabbitBootstrapConfiguration.java | 7 +- ...itListenerAnnotationBeanPostProcessor.java | 16 +- .../RabbitBootstrapConfiguration.java | 5 +- ...itListenerAnnotationBeanPostProcessor.java | 95 ++--- .../amqp/rabbit/annotation/package-info.java | 1 + .../amqp/rabbit/aot/RabbitRuntimeHints.java | 7 +- .../amqp/rabbit/aot/package-info.java | 3 +- .../amqp/rabbit/batch/BatchingStrategy.java | 8 +- .../amqp/rabbit/batch/MessageBatch.java | 34 +- .../rabbit/batch/SimpleBatchingStrategy.java | 14 +- .../amqp/rabbit/batch/package-info.java | 1 + .../rabbit/config/AbstractExchangeParser.java | 15 +- ...bstractRabbitListenerContainerFactory.java | 122 +++--- ...RetryOperationsInterceptorFactoryBean.java | 11 +- .../rabbit/config/AnnotationDrivenParser.java | 9 +- .../BaseRabbitListenerContainerFactory.java | 53 +-- .../rabbit/config/BindingFactoryBean.java | 23 +- .../DirectRabbitListenerContainerFactory.java | 24 +- .../config/ListenerContainerFactoryBean.java | 121 +++--- .../config/ListenerContainerParser.java | 14 +- .../amqp/rabbit/config/NamespaceUtils.java | 20 +- .../amqp/rabbit/config/QueueParser.java | 7 +- .../rabbit/config/RabbitNamespaceUtils.java | 5 +- .../config/RetryInterceptorBuilder.java | 22 +- .../SimpleRabbitListenerContainerFactory.java | 57 +-- .../config/SimpleRabbitListenerEndpoint.java | 10 +- ...RetryOperationsInterceptorFactoryBean.java | 21 +- ...RetryOperationsInterceptorFactoryBean.java | 13 +- .../amqp/rabbit/config/TemplateParser.java | 20 +- .../amqp/rabbit/config/package-info.java | 1 + .../connection/AbstractConnectionFactory.java | 40 +- .../AbstractRoutingConnectionFactory.java | 38 +- .../AfterCompletionFailedException.java | 5 +- .../connection/CachingConnectionFactory.java | 165 ++++---- .../rabbit/connection/ChannelListener.java | 4 +- .../amqp/rabbit/connection/ChannelProxy.java | 2 +- .../CompositeConnectionListener.java | 5 +- .../amqp/rabbit/connection/Connection.java | 9 +- .../rabbit/connection/ConnectionFactory.java | 5 +- .../ConnectionFactoryConfigurationUtils.java | 6 +- .../ConnectionFactoryContextWrapper.java | 5 +- .../connection/ConnectionFactoryUtils.java | 50 +-- .../rabbit/connection/ConnectionListener.java | 7 +- .../rabbit/connection/ConnectionProxy.java | 8 +- .../connection/ConsumerChannelRegistry.java | 49 +-- .../rabbit/connection/CorrelationData.java | 44 +-- .../amqp/rabbit/connection/FactoryFinder.java | 7 +- .../LocalizedQueueConnectionFactory.java | 53 ++- .../amqp/rabbit/connection/NodeLocator.java | 20 +- .../rabbit/connection/PendingConfirm.java | 15 +- .../PooledChannelConnectionFactory.java | 56 ++- .../PublisherCallbackChannelImpl.java | 44 +-- .../rabbit/connection/RabbitAccessor.java | 9 +- .../RabbitConnectionFactoryBean.java | 66 ++-- .../connection/RabbitResourceHolder.java | 15 +- .../amqp/rabbit/connection/RabbitUtils.java | 8 +- .../connection/RestTemplateNodeLocator.java | 8 +- .../connection/RoutingConnectionFactory.java | 4 +- .../rabbit/connection/SimpleConnection.java | 17 +- ...lePropertyValueConnectionNameStrategy.java | 8 +- .../connection/SimpleResourceHolder.java | 25 +- .../SimpleRoutingConnectionFactory.java | 8 +- .../ThreadChannelConnectionFactory.java | 93 ++--- .../rabbit/connection/WebFluxNodeLocator.java | 8 +- .../amqp/rabbit/connection/package-info.java | 2 +- .../core/AmqpNackReceivedException.java | 5 +- .../rabbit/core/BatchingRabbitTemplate.java | 13 +- .../amqp/rabbit/core/BrokerEvent.java | 14 +- .../amqp/rabbit/core/BrokerEventListener.java | 19 +- .../amqp/rabbit/core/ChannelCallback.java | 3 +- .../core/CorrelationDataPostProcessor.java | 8 +- .../core/DeclarationExceptionEvent.java | 13 +- .../DeclareExchangeConnectionListener.java | 13 +- .../amqp/rabbit/core/RabbitAdmin.java | 47 ++- .../amqp/rabbit/core/RabbitAdminEvent.java | 5 +- .../rabbit/core/RabbitGatewaySupport.java | 11 +- .../rabbit/core/RabbitMessageOperations.java | 26 +- .../rabbit/core/RabbitMessagingTemplate.java | 45 +-- .../amqp/rabbit/core/RabbitOperations.java | 57 ++- .../amqp/rabbit/core/RabbitTemplate.java | 372 +++++++----------- .../amqp/rabbit/core/package-info.java | 2 +- .../AbstractMessageListenerContainer.java | 135 +++---- .../AbstractRabbitListenerEndpoint.java | 84 ++-- .../listener/BlockingQueueConsumer.java | 57 +-- .../ConditionalRejectingErrorHandler.java | 15 +- .../DirectMessageListenerContainer.java | 106 ++--- ...DirectReplyToMessageListenerContainer.java | 3 +- .../ListenerContainerConsumerFailedEvent.java | 20 +- ...tenerContainerConsumerTerminatedEvent.java | 13 +- .../listener/ListenerContainerIdleEvent.java | 12 +- ...erFailedRuleBasedTransactionAttribute.java | 6 +- .../rabbit/listener/MessageAckListener.java | 9 +- .../listener/MessageListenerContainer.java | 7 +- .../MethodRabbitListenerEndpoint.java | 61 +-- .../rabbit/listener/MicrometerHolder.java | 14 +- .../rabbit/listener/MissingQueueEvent.java | 5 +- .../MultiMethodRabbitListenerEndpoint.java | 22 +- .../listener/ObservableListenerContainer.java | 25 +- .../RabbitListenerContainerFactory.java | 5 +- .../listener/RabbitListenerEndpoint.java | 29 +- .../RabbitListenerEndpointRegistrar.java | 46 +-- .../RabbitListenerEndpointRegistry.java | 16 +- .../SimpleMessageListenerContainer.java | 141 ++++--- .../AbstractAdaptableMessageListener.java | 120 +++--- .../AmqpMessageHandlerMethodFactory.java | 12 +- .../BatchMessagingMessageListenerAdapter.java | 28 +- .../adapter/DelegatingInvocableHandler.java | 84 ++-- .../listener/adapter/HandlerAdapter.java | 34 +- .../listener/adapter/InvocationResult.java | 25 +- .../KotlinAwareInvocableHandlerMethod.java | 6 +- .../adapter/MessageListenerAdapter.java | 39 +- .../MessagingMessageListenerAdapter.java | 128 +++--- .../rabbit/listener/adapter/MonoHandler.java | 9 +- .../rabbit/listener/adapter/package-info.java | 1 + .../api/ChannelAwareBatchMessageListener.java | 5 +- .../api/ChannelAwareMessageListener.java | 4 +- .../api/RabbitListenerErrorHandler.java | 8 +- .../rabbit/listener/api/package-info.java | 1 + .../listener/exception/package-info.java | 1 + .../amqp/rabbit/listener/package-info.java | 2 +- .../rabbit/listener/support/package-info.java | 1 + .../amqp/rabbit/log4j2/AmqpAppender.java | 175 ++++---- .../amqp/rabbit/log4j2/package-info.java | 1 + .../amqp/rabbit/logback/AmqpAppender.java | 138 ++++--- .../amqp/rabbit/logback/package-info.java | 1 + .../amqp/rabbit/package-info.java | 1 + .../retry/RepublishMessageRecoverer.java | 30 +- ...RepublishMessageRecovererWithConfirms.java | 13 +- .../amqp/rabbit/retry/package-info.java | 1 + .../support/ConsumerCancelledException.java | 5 +- .../DefaultMessagePropertiesConverter.java | 50 +-- .../support/ListenerContainerAware.java | 5 +- .../ListenerExecutionFailedException.java | 9 +- .../support/MessagePropertiesConverter.java | 2 +- .../support/RabbitExceptionTranslator.java | 5 +- .../amqp/rabbit/support/ValueExpression.java | 83 ++-- .../RabbitMessageReceiverContext.java | 6 +- .../RabbitMessageSenderContext.java | 13 +- .../support/micrometer/package-info.java | 3 +- .../amqp/rabbit/support/package-info.java | 2 +- .../transaction/RabbitTransactionManager.java | 76 ++-- .../amqp/rabbit/transaction/package-info.java | 1 + .../EnableRabbitIntegrationTests.java | 5 +- .../config/MessageListenerTestContainer.java | 8 +- .../CachingConnectionFactoryTests.java | 26 +- .../rabbit/connection/NodeLocatorTests.java | 31 +- .../PooledChannelConnectionFactoryTests.java | 15 +- .../connection/SingleConnectionFactory.java | 44 +-- .../core/BatchingRabbitTemplateTests.java | 10 +- .../core/MessagingTemplateConfirmsTests.java | 4 +- .../core/RabbitMessagingTemplateTests.java | 2 + .../core/RabbitTemplateIntegrationTests.java | 36 +- ...tePublisherCallbacksIntegration1Tests.java | 18 +- ...tePublisherCallbacksIntegration2Tests.java | 4 +- ...tingConnectionFactoryIntegrationTests.java | 2 +- .../listener/ExternalTxManagerTests.java | 1 - .../listener/LocallyTransactedTests.java | 1 - ...ContainerErrorHandlerIntegrationTests.java | 16 +- .../MethodRabbitListenerEndpointTests.java | 48 +-- ...ageListenerContainerIntegration2Tests.java | 2 +- .../SimpleMessageListenerContainerTests.java | 144 +------ .../support/micrometer/ObservationTests.java | 2 +- .../annotation/EnableRabbitKotlinTests.kt | 11 +- src/checkstyle/checkstyle.xml | 4 +- 273 files changed, 3257 insertions(+), 3171 deletions(-) create mode 100644 spring-amqp/src/main/java/org/springframework/amqp/utils/test/package-info.java diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 249a97f0c4..609184afcb 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -19,7 +19,6 @@ updates: - patch patterns: - com.gradle.* - - com.github.spotbugs - io.spring.* - org.ajoberstar.grgit - org.antora @@ -28,6 +27,9 @@ updates: - org.hibernate.validator:hibernate-validator - org.apache.httpcomponents.client5:httpclient5 - org.awaitility:awaitility + - net.ltgt.errorprone + - com.uber.nullaway* + - com.google.errorprone* - package-ecosystem: github-actions directory: / diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index c9f71e8bdf..cefa6bf31a 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -8,4 +8,4 @@ on: jobs: build-pull-request: - uses: spring-io/spring-github-workflows/.github/workflows/spring-gradle-pull-request-build.yml@v5 + uses: spring-io/spring-github-workflows/.github/workflows/spring-gradle-pull-request-build.yml@main diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be8a9e40c6..29383edbb7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,9 @@ jobs: contents: write issues: write - uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-release.yml@v5 + uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-gradle-release.yml@main + with: + deployMilestoneToCentral: true secrets: GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }} diff --git a/build.gradle b/build.gradle index 0522432d42..71d2b038e8 100644 --- a/build.gradle +++ b/build.gradle @@ -19,8 +19,8 @@ plugins { id 'io.spring.dependency-management' version '1.1.7' apply false id 'org.antora' version '1.0.0' id 'io.spring.antora.generate-antora-yml' version '0.0.1' - id 'com.github.spotbugs' version '6.1.3' id 'io.freefair.aggregate-javadoc' version '8.11' + id 'net.ltgt.errorprone' version '4.1.0' apply false } description = 'Spring AMQP' @@ -73,12 +73,12 @@ ext { antora { version = '3.2.0-alpha.2' playbook = file('src/reference/antora/antora-playbook.yml') - options = ['to-dir' : project.layout.buildDirectory.dir('site').get().toString(), clean: true, fetch: !project.gradle.startParameter.offline, stacktrace: true] + options = ['to-dir': project.layout.buildDirectory.dir('site').get().toString(), clean: true, fetch: !project.gradle.startParameter.offline, stacktrace: true] dependencies = [ - '@antora/atlas-extension': '1.0.0-alpha.2', - '@antora/collector-extension': '1.0.0-beta.3', - '@asciidoctor/tabs': '1.0.0-beta.6', - '@springio/antora-extensions': '1.14.2', + '@antora/atlas-extension' : '1.0.0-alpha.2', + '@antora/collector-extension' : '1.0.0-beta.3', + '@asciidoctor/tabs' : '1.0.0-beta.6', + '@springio/antora-extensions' : '1.14.2', '@springio/asciidoctor-extensions': '1.0.0-alpha.14', ] } @@ -158,10 +158,14 @@ configure(javaProjects) { subproject -> apply plugin: 'checkstyle' apply plugin: 'kotlin' apply plugin: 'kotlin-spring' + apply plugin: 'net.ltgt.errorprone' apply from: "${rootProject.projectDir}/gradle/publish-maven.gradle" java { + toolchain { + languageVersion = JavaLanguageVersion.of(23) + } withJavadocJar() withSourcesJar() registerFeature('optional') { @@ -169,14 +173,27 @@ configure(javaProjects) { subproject -> } } - compileJava { - options.release = 17 - } - - compileTestJava { + tasks.withType(JavaCompile) { sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 options.encoding = 'UTF-8' + options.errorprone { + disableAllChecks = true + if (!name.toLowerCase().contains('test')) { + option('NullAway:OnlyNullMarked', 'true') + option('NullAway:CustomContractAnnotations', 'org.springframework.lang.Contract') + option('NullAway:JSpecifyMode', 'true') + error('NullAway') + } + } + } + + compileTestKotlin { + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17 + } + compilerOptions { + allWarningsAsErrors = true + } } eclipse { @@ -187,10 +204,6 @@ configure(javaProjects) { subproject -> // dependencies that are common across all java projects dependencies { - def spotbugsAnnotations = "com.github.spotbugs:spotbugs-annotations:${spotbugs.toolVersion.get()}" - compileOnly spotbugsAnnotations - testCompileOnly spotbugsAnnotations - testImplementation 'org.apache.logging.log4j:log4j-core' testImplementation "org.hamcrest:hamcrest-core:$hamcrestVersion" testImplementation("org.mockito:mockito-core:$mockitoVersion") { @@ -211,6 +224,9 @@ configure(javaProjects) { subproject -> testImplementation 'org.jetbrains.kotlin:kotlin-reflect' testImplementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + + errorprone 'com.uber.nullaway:nullaway:0.12.3' + errorprone 'com.google.errorprone:error_prone_core:2.36.0' } // enable all compiler warnings; individual projects may customize further @@ -430,12 +446,6 @@ project('spring-rabbit') { exclude group: 'org.hamcrest', module: 'hamcrest-core' } } - - compileKotlin { - compilerOptions { - allWarningsAsErrors = true - } - } } project('spring-rabbit-stream') { diff --git a/spring-amqp/src/main/java/org/springframework/amqp/AmqpException.java b/spring-amqp/src/main/java/org/springframework/amqp/AmqpException.java index 7e6aea5128..9e329c8810 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/AmqpException.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/AmqpException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -16,10 +16,13 @@ package org.springframework.amqp; +import org.jspecify.annotations.Nullable; + /** * Base RuntimeException for errors that occur when executing AMQP operations. * * @author Mark Fisher + * @author Artem Bilan */ @SuppressWarnings("serial") public class AmqpException extends RuntimeException { @@ -28,11 +31,11 @@ public AmqpException(String message) { super(message); } - public AmqpException(Throwable cause) { + public AmqpException(@Nullable Throwable cause) { super(cause); } - public AmqpException(String message, Throwable cause) { + public AmqpException(@Nullable String message, @Nullable Throwable cause) { super(message, cause); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/AmqpRejectAndDontRequeueException.java b/spring-amqp/src/main/java/org/springframework/amqp/AmqpRejectAndDontRequeueException.java index 168e39d6cd..56863b5a28 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/AmqpRejectAndDontRequeueException.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/AmqpRejectAndDontRequeueException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -16,13 +16,16 @@ package org.springframework.amqp; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Exception for listener implementations used to indicate the - * basic.reject will be sent with requeue=false in order to enable - * features such as DLQ. + * {@code basic.reject} will be sent with {@code requeue=false} + * in order to enable features such as DLQ. + * * @author Gary Russell + * @author Artem Bilan + * * @since 1.0.1 * */ diff --git a/spring-amqp/src/main/java/org/springframework/amqp/UncategorizedAmqpException.java b/spring-amqp/src/main/java/org/springframework/amqp/UncategorizedAmqpException.java index 3612bd06bd..06e33d08d2 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/UncategorizedAmqpException.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/UncategorizedAmqpException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -16,6 +16,8 @@ package org.springframework.amqp; +import org.jspecify.annotations.Nullable; + /** * A "catch-all" exception type within the AmqpException hierarchy * when no more specific cause is known. @@ -25,7 +27,7 @@ @SuppressWarnings("serial") public class UncategorizedAmqpException extends AmqpException { - public UncategorizedAmqpException(Throwable cause) { + public UncategorizedAmqpException(@Nullable Throwable cause) { super(cause); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractBuilder.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractBuilder.java index 30506f62ca..add559ba60 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractBuilder.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 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. @@ -19,30 +19,34 @@ import java.util.LinkedHashMap; import java.util.Map; +import org.jspecify.annotations.Nullable; + /** * Base class for builders supporting arguments. * * @author Gary Russell * @author Ngoc Nhan + * @author Artem Bilan + * * @since 1.6 * */ public abstract class AbstractBuilder { - private Map arguments; + private @Nullable Map arguments; /** * Return the arguments map, after creating one if necessary. * @return the arguments. */ - protected Map getOrCreateArguments() { + protected Map getOrCreateArguments() { if (this.arguments == null) { this.arguments = new LinkedHashMap<>(); } return this.arguments; } - protected Map getArguments() { + protected @Nullable Map getArguments() { return this.arguments; } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractDeclarable.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractDeclarable.java index 2a18103b5e..fd640038b1 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractDeclarable.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractDeclarable.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -25,7 +25,8 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -41,7 +42,7 @@ public abstract class AbstractDeclarable implements Declarable { private final Lock lock = new ReentrantLock(); - private final Map arguments; + protected final Map arguments; private boolean shouldDeclare = true; @@ -58,7 +59,7 @@ public AbstractDeclarable() { * @param arguments the arguments. * @since 2.2.2 */ - public AbstractDeclarable(@Nullable Map arguments) { + public AbstractDeclarable(@Nullable Map arguments) { if (arguments != null) { this.arguments = new HashMap<>(arguments); } @@ -101,7 +102,8 @@ public void setIgnoreDeclarationExceptions(boolean ignoreDeclarationExceptions) } @Override - public void setAdminsThatShouldDeclare(@Nullable Object... adminArgs) { + @SuppressWarnings("NullAway") // Dataflow analysis limitation + public void setAdminsThatShouldDeclare(@Nullable Object @Nullable ... adminArgs) { Collection admins = new ArrayList<>(); if (adminArgs != null) { if (adminArgs.length > 1) { diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractExchange.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractExchange.java index 9accd5d8a1..57dc6c41e6 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractExchange.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AbstractExchange.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -18,6 +18,7 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; /** * Common properties that describe all exchange types. @@ -71,7 +72,9 @@ public AbstractExchange(String name, boolean durable, boolean autoDelete) { * longer in use * @param arguments the arguments used to declare the exchange */ - public AbstractExchange(String name, boolean durable, boolean autoDelete, Map arguments) { + public AbstractExchange(String name, boolean durable, boolean autoDelete, + @Nullable Map arguments) { + super(arguments); this.name = name; this.durable = durable; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpAdmin.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpAdmin.java index 0968cf9a08..97117fbfa2 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpAdmin.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpAdmin.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -20,8 +20,7 @@ import java.util.Properties; import java.util.Set; -import org.springframework.lang.Nullable; - +import org.jspecify.annotations.Nullable; /** * Specifies a basic set of portable AMQP administrative operations for AMQP > 0.9. diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpMessageReturnedException.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpMessageReturnedException.java index d831fef822..4cc5bea3c9 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpMessageReturnedException.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpMessageReturnedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2022 the original author or authors. + * Copyright 2015-2025 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. @@ -16,6 +16,8 @@ package org.springframework.amqp.core; +import java.io.Serial; + import org.springframework.amqp.AmqpException; /** @@ -28,9 +30,10 @@ */ public class AmqpMessageReturnedException extends AmqpException { + @Serial private static final long serialVersionUID = 1866579721126554167L; - private final ReturnedMessage returned; + private final transient ReturnedMessage returned; public AmqpMessageReturnedException(String message, ReturnedMessage returned) { super(message); diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpTemplate.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpTemplate.java index 3037e54096..1cd0dec7ac 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpTemplate.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -16,13 +16,14 @@ package org.springframework.amqp.core; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.AmqpException; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.lang.Nullable; /** * Specifies a basic set of AMQP operations. - * + *

* Provides synchronous send and receive methods. The {@link #convertAndSend(Object)} and * {@link #receiveAndConvert()} methods allow let you send and receive POJO objects. * Implementations are expected to delegate to an instance of @@ -34,6 +35,7 @@ * @author Artem Bilan * @author Ernest Sadykov * @author Gary Russell + * @author Artem Bilan */ public interface AmqpTemplate { @@ -250,8 +252,7 @@ void convertAndSend(String exchange, String routingKey, Object message, MessageP * @throws AmqpException if there is a problem. * @since 2.0 */ - @Nullable - T receiveAndConvert(ParameterizedTypeReference type) throws AmqpException; + @Nullable T receiveAndConvert(ParameterizedTypeReference type) throws AmqpException; /** * Receive a message if there is one from a specific queue and convert it to a Java @@ -265,8 +266,7 @@ void convertAndSend(String exchange, String routingKey, Object message, MessageP * @throws AmqpException if there is a problem * @since 2.0 */ - @Nullable - T receiveAndConvert(String queueName, ParameterizedTypeReference type) throws AmqpException; + @Nullable T receiveAndConvert(String queueName, ParameterizedTypeReference type) throws AmqpException; /** * Receive a message if there is one from a default queue and convert it to a Java @@ -283,8 +283,7 @@ void convertAndSend(String exchange, String routingKey, Object message, MessageP * @throws AmqpException if there is a problem * @since 2.0 */ - @Nullable - T receiveAndConvert(long timeoutMillis, ParameterizedTypeReference type) throws AmqpException; + @Nullable T receiveAndConvert(long timeoutMillis, ParameterizedTypeReference type) throws AmqpException; /** * Receive a message if there is one from a specific queue and convert it to a Java @@ -302,8 +301,7 @@ void convertAndSend(String exchange, String routingKey, Object message, MessageP * @throws AmqpException if there is a problem * @since 2.0 */ - @Nullable - T receiveAndConvert(String queueName, long timeoutMillis, ParameterizedTypeReference type) + @Nullable T receiveAndConvert(String queueName, long timeoutMillis, ParameterizedTypeReference type) throws AmqpException; // receive and send methods for provided callback @@ -559,8 +557,7 @@ Object convertSendAndReceive(String exchange, String routingKey, Object message, * @throws AmqpException if there is a problem. * @since 2.0 */ - @Nullable - T convertSendAndReceiveAsType(Object message, ParameterizedTypeReference responseType) + @Nullable T convertSendAndReceiveAsType(Object message, ParameterizedTypeReference responseType) throws AmqpException; /** @@ -577,8 +574,7 @@ T convertSendAndReceiveAsType(Object message, ParameterizedTypeReference * @throws AmqpException if there is a problem. * @since 2.0 */ - @Nullable - T convertSendAndReceiveAsType(String routingKey, Object message, + @Nullable T convertSendAndReceiveAsType(String routingKey, Object message, ParameterizedTypeReference responseType) throws AmqpException; /** @@ -592,12 +588,11 @@ T convertSendAndReceiveAsType(String routingKey, Object message, * @param message a message to send. * @param responseType the type to convert the reply to. * @param the type. - * @return the response; or null if the reply times out. + * @return the response; or null if the reply times out. * @throws AmqpException if there is a problem. * @since 2.0 */ - @Nullable - T convertSendAndReceiveAsType(String exchange, String routingKey, Object message, + @Nullable T convertSendAndReceiveAsType(String exchange, String routingKey, Object message, ParameterizedTypeReference responseType) throws AmqpException; /** @@ -614,8 +609,7 @@ T convertSendAndReceiveAsType(String exchange, String routingKey, Object mes * @throws AmqpException if there is a problem. * @since 2.0 */ - @Nullable - T convertSendAndReceiveAsType(Object message, MessagePostProcessor messagePostProcessor, + @Nullable T convertSendAndReceiveAsType(Object message, MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) throws AmqpException; /** @@ -633,8 +627,7 @@ T convertSendAndReceiveAsType(Object message, MessagePostProcessor messagePo * @throws AmqpException if there is a problem. * @since 2.0 */ - @Nullable - T convertSendAndReceiveAsType(String routingKey, Object message, + @Nullable T convertSendAndReceiveAsType(String routingKey, Object message, MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) throws AmqpException; @@ -654,8 +647,7 @@ T convertSendAndReceiveAsType(String routingKey, Object message, * @throws AmqpException if there is a problem. * @since 2.0 */ - @Nullable - T convertSendAndReceiveAsType(String exchange, String routingKey, Object message, + @Nullable T convertSendAndReceiveAsType(String exchange, String routingKey, Object message, MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) throws AmqpException; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AnonymousQueue.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AnonymousQueue.java index e4ea9c52bf..fd0390c6d6 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AnonymousQueue.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AnonymousQueue.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 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. @@ -18,14 +18,17 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + /** * Represents an anonymous, non-durable, exclusive, auto-delete queue. The name has the - * form 'spring.gen-<base64UUID>' by default, but it can be modified by providing a + * form {@code spring.gen-} by default, but it can be modified by providing a * {@link NamingStrategy}. Two naming strategies {@link Base64UrlNamingStrategy} and - * {@link UUIDNamingStrategy} are provided but you can implement your own. Names should be + * {@link UUIDNamingStrategy} are provided, but you can implement your own. Names should be * unique. * @author Dave Syer * @author Gary Russell + * @author Artem Bilan * */ public class AnonymousQueue extends Queue { @@ -34,15 +37,15 @@ public class AnonymousQueue extends Queue { * Construct a queue with a Base64-based name. */ public AnonymousQueue() { - this((Map) null); + this((Map) null); } /** * Construct a queue with a Base64-based name with the supplied arguments. * @param arguments the arguments. */ - public AnonymousQueue(Map arguments) { - this(org.springframework.amqp.core.Base64UrlNamingStrategy.DEFAULT, arguments); + public AnonymousQueue(@Nullable Map arguments) { + this(Base64UrlNamingStrategy.DEFAULT, arguments); } /** @@ -62,9 +65,12 @@ public AnonymousQueue(org.springframework.amqp.core.NamingStrategy namingStrateg * @param arguments the arguments. * @since 2.1 */ - public AnonymousQueue(org.springframework.amqp.core.NamingStrategy namingStrategy, Map arguments) { + @SuppressWarnings("this-escape") + public AnonymousQueue(org.springframework.amqp.core.NamingStrategy namingStrategy, + @Nullable Map arguments) { + super(namingStrategy.generateName(), false, true, true, arguments); - if (!getArguments().containsKey(X_QUEUE_LEADER_LOCATOR)) { + if (!this.arguments.containsKey(X_QUEUE_LEADER_LOCATOR)) { setLeaderLocator("client-local"); } } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate.java index 123c3b6fa5..3474a65870 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-2025 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. @@ -18,6 +18,8 @@ import java.util.concurrent.CompletableFuture; +import org.jspecify.annotations.Nullable; + import org.springframework.core.ParameterizedTypeReference; /** @@ -123,7 +125,7 @@ CompletableFuture convertSendAndReceive(String routingKey, Object object, * @return the {@link CompletableFuture}. */ CompletableFuture convertSendAndReceive(String exchange, String routingKey, Object object, - MessagePostProcessor messagePostProcessor); + @Nullable MessagePostProcessor messagePostProcessor); /** * Convert the object to a message and send it to the default exchange with the @@ -185,7 +187,7 @@ CompletableFuture convertSendAndReceiveAsType(Object object, MessagePostP * @return the {@link CompletableFuture}. */ CompletableFuture convertSendAndReceiveAsType(String routingKey, Object object, - MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType); + @Nullable MessagePostProcessor messagePostProcessor, @Nullable ParameterizedTypeReference responseType); /** * Convert the object to a message and send it to the provided exchange and @@ -200,6 +202,6 @@ CompletableFuture convertSendAndReceiveAsType(String routingKey, Object o * @return the {@link CompletableFuture}. */ CompletableFuture convertSendAndReceiveAsType(String exchange, String routingKey, Object object, - MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType); + @Nullable MessagePostProcessor messagePostProcessor, @Nullable ParameterizedTypeReference responseType); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/BaseExchangeBuilder.java b/spring-amqp/src/main/java/org/springframework/amqp/core/BaseExchangeBuilder.java index 0e956e034c..99ee99d17a 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/BaseExchangeBuilder.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/BaseExchangeBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2024-2025 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. @@ -19,6 +19,8 @@ import java.util.Arrays; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -51,7 +53,7 @@ public abstract class BaseExchangeBuilder> exte private boolean declare = true; - private Object[] declaringAdmins; + private @Nullable Object @Nullable [] declaringAdmins; /** * Construct an instance of the appropriate type. @@ -101,7 +103,7 @@ public B withArgument(String key, Object value) { * @param arguments the arguments map. * @return the builder. */ - public B withArguments(Map arguments) { + public B withArguments(Map arguments) { this.getOrCreateArguments().putAll(arguments); return _this(); } @@ -163,27 +165,17 @@ public B admins(Object... admins) { @SuppressWarnings("unchecked") public T build() { - AbstractExchange exchange; - if (ExchangeTypes.DIRECT.equals(this.type)) { - exchange = new DirectExchange(this.name, this.durable, this.autoDelete, getArguments()); - } - else if (ExchangeTypes.TOPIC.equals(this.type)) { - exchange = new TopicExchange(this.name, this.durable, this.autoDelete, getArguments()); - } - else if (ExchangeTypes.FANOUT.equals(this.type)) { - exchange = new FanoutExchange(this.name, this.durable, this.autoDelete, getArguments()); - } - else if (ExchangeTypes.HEADERS.equals(this.type)) { - exchange = new HeadersExchange(this.name, this.durable, this.autoDelete, getArguments()); - } - else { - exchange = new CustomExchange(this.name, this.type, this.durable, this.autoDelete, getArguments()); - } + AbstractExchange exchange = switch (this.type) { + case ExchangeTypes.DIRECT -> new DirectExchange(this.name, this.durable, this.autoDelete, getArguments()); + case ExchangeTypes.TOPIC -> new TopicExchange(this.name, this.durable, this.autoDelete, getArguments()); + case ExchangeTypes.FANOUT -> new FanoutExchange(this.name, this.durable, this.autoDelete, getArguments()); + case ExchangeTypes.HEADERS -> new HeadersExchange(this.name, this.durable, this.autoDelete, getArguments()); + default -> new CustomExchange(this.name, this.type, this.durable, this.autoDelete, getArguments()); + }; return (T) configureExchange(exchange); } - protected T configureExchange(T exchange) { exchange.setInternal(this.internal); exchange.setDelayed(this.delayed); diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/Binding.java b/spring-amqp/src/main/java/org/springframework/amqp/core/Binding.java index ac2ac58acd..3008323f94 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/Binding.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/Binding.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -18,7 +18,8 @@ import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -31,6 +32,7 @@ * @author Dave Syer * @author Gary Russell * @author Ngoc Nhan + * @author Artem Bilan * * @see AmqpAdmin */ @@ -52,27 +54,24 @@ public enum DestinationType { EXCHANGE; } - @Nullable - private final String destination; + private final @Nullable String destination; - private final String exchange; + private final @Nullable String exchange; - @Nullable - private final String routingKey; + private final @Nullable String routingKey; private final DestinationType destinationType; - @Nullable - private final Queue lazyQueue; + private final @Nullable Queue lazyQueue; - public Binding(String destination, DestinationType destinationType, String exchange, String routingKey, - @Nullable Map arguments) { + public Binding(String destination, DestinationType destinationType, @Nullable String exchange, String routingKey, + @Nullable Map arguments) { this(null, destination, destinationType, exchange, routingKey, arguments); } public Binding(@Nullable Queue lazyQueue, @Nullable String destination, DestinationType destinationType, - String exchange, @Nullable String routingKey, @Nullable Map arguments) { + @Nullable String exchange, @Nullable String routingKey, @Nullable Map arguments) { super(arguments); Assert.isTrue(lazyQueue == null || destinationType == DestinationType.QUEUE, @@ -85,7 +84,7 @@ public Binding(@Nullable Queue lazyQueue, @Nullable String destination, Destinat this.routingKey = routingKey; } - public String getDestination() { + public @Nullable String getDestination() { if (this.lazyQueue != null) { return this.lazyQueue.getActualName(); } @@ -98,11 +97,11 @@ public DestinationType getDestinationType() { return this.destinationType; } - public String getExchange() { + public @Nullable String getExchange() { return this.exchange; } - public String getRoutingKey() { + public @Nullable String getRoutingKey() { if (this.routingKey == null && this.lazyQueue != null) { return this.lazyQueue.getActualName(); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/BindingBuilder.java b/spring-amqp/src/main/java/org/springframework/amqp/core/BindingBuilder.java index d297f16d9d..cacfc3920c 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/BindingBuilder.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/BindingBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -16,10 +16,11 @@ package org.springframework.amqp.core; -import java.util.Collections; import java.util.HashMap; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Binding.DestinationType; import org.springframework.util.Assert; @@ -31,6 +32,7 @@ * @author Dave Syer * @author Gary Russell * @author Ngoc Nhan + * @author Artem Bilan */ public final class BindingBuilder { @@ -38,7 +40,7 @@ private BindingBuilder() { } public static DestinationConfigurer bind(Queue queue) { - if ("".equals(queue.getName())) { + if (queue.getName().isEmpty()) { return new DestinationConfigurer(queue, DestinationType.QUEUE); } else { @@ -50,8 +52,8 @@ public static DestinationConfigurer bind(Exchange exchange) { return new DestinationConfigurer(exchange.getName(), DestinationType.EXCHANGE); } - private static Map createMapForKeys(String... keys) { - Map map = new HashMap<>(); + private static Map createMapForKeys(String... keys) { + Map map = new HashMap<>(); for (String key : keys) { map.put(key, null); } @@ -63,11 +65,11 @@ private static Map createMapForKeys(String... keys) { */ public static final class DestinationConfigurer { - protected final String name; // NOSONAR + protected final @Nullable String name; // NOSONAR protected final DestinationType type; // NOSONAR - protected final Queue queue; // NOSONAR + protected final @Nullable Queue queue; // NOSONAR DestinationConfigurer(String name, DestinationType type) { this.queue = null; @@ -100,6 +102,7 @@ public TopicExchangeRoutingKeyConfigurer to(TopicExchange exchange) { public GenericExchangeRoutingKeyConfigurer to(Exchange exchange) { return new GenericExchangeRoutingKeyConfigurer(this, exchange); } + } /** @@ -156,13 +159,14 @@ public Binding exists() { } public Binding matches(Object value) { - Map map = new HashMap<>(); + Map map = new HashMap<>(); map.put(this.key, value); return new Binding(HeadersExchangeMapConfigurer.this.destination.queue, HeadersExchangeMapConfigurer.this.destination.name, HeadersExchangeMapConfigurer.this.destination.type, HeadersExchangeMapConfigurer.this.exchange.getName(), "", map); } + } /** @@ -170,7 +174,7 @@ public Binding matches(Object value) { */ public final class HeadersExchangeKeysBindingCreator { - private final Map headerMap; + private final Map headerMap; HeadersExchangeKeysBindingCreator(String[] headerKeys, boolean matchAll) { Assert.notEmpty(headerKeys, "header key list must not be empty"); @@ -184,6 +188,7 @@ public Binding exist() { HeadersExchangeMapConfigurer.this.destination.type, HeadersExchangeMapConfigurer.this.exchange.getName(), "", this.headerMap); } + } /** @@ -191,7 +196,7 @@ public Binding exist() { */ public final class HeadersExchangeMapBindingCreator { - private final Map headerMap; + private final Map headerMap; HeadersExchangeMapBindingCreator(Map headerMap, boolean matchAll) { Assert.notEmpty(headerMap, "header map must not be empty"); @@ -205,7 +210,9 @@ public Binding match() { HeadersExchangeMapConfigurer.this.destination.type, HeadersExchangeMapConfigurer.this.exchange.getName(), "", this.headerMap); } + } + } private abstract static class AbstractRoutingKeyConfigurer { @@ -218,6 +225,7 @@ private abstract static class AbstractRoutingKeyConfigurer { this.destination = destination; this.exchange = exchange; } + } /** @@ -230,14 +238,14 @@ public static final class TopicExchangeRoutingKeyConfigurer extends AbstractRout } public Binding with(String routingKey) { - return new Binding(destination.queue, destination.name, destination.type, exchange, routingKey, - Collections.emptyMap()); + return new Binding(destination.queue, destination.name, destination.type, exchange, routingKey, null); } public Binding with(Enum routingKeyEnum) { return new Binding(destination.queue, destination.name, destination.type, exchange, - routingKeyEnum.toString(), Collections.emptyMap()); + routingKeyEnum.toString(), null); } + } /** @@ -273,7 +281,7 @@ public GenericArgumentsConfigurer(GenericExchangeRoutingKeyConfigurer configurer this.routingKey = routingKey; } - public Binding and(Map map) { + public Binding and(Map map) { return new Binding(this.configurer.destination.queue, this.configurer.destination.name, this.configurer.destination.type, this.configurer.exchange, this.routingKey, map); @@ -282,7 +290,7 @@ public Binding and(Map map) { public Binding noargs() { return new Binding(this.configurer.destination.queue, this.configurer.destination.name, this.configurer.destination.type, this.configurer.exchange, - this.routingKey, Collections.emptyMap()); + this.routingKey, null); } } @@ -297,18 +305,16 @@ public static final class DirectExchangeRoutingKeyConfigurer extends AbstractRou } public Binding with(String routingKey) { - return new Binding(destination.queue, destination.name, destination.type, exchange, routingKey, - Collections.emptyMap()); + return new Binding(destination.queue, destination.name, destination.type, exchange, routingKey, null); } public Binding with(Enum routingKeyEnum) { return new Binding(destination.queue, destination.name, destination.type, exchange, - routingKeyEnum.toString(), Collections.emptyMap()); + routingKeyEnum.toString(), null); } public Binding withQueueName() { - return new Binding(destination.queue, destination.name, destination.type, exchange, destination.name, - Collections.emptyMap()); + return new Binding(destination.queue, destination.name, destination.type, exchange, destination.name, null); } } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/ConsistentHashExchange.java b/spring-amqp/src/main/java/org/springframework/amqp/core/ConsistentHashExchange.java index 47eefd7a85..24402bd058 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/ConsistentHashExchange.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/ConsistentHashExchange.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2024-2025 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. @@ -18,6 +18,8 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -61,9 +63,12 @@ public ConsistentHashExchange(String name, boolean durable, boolean autoDelete) * longer in use * @param arguments the arguments used to declare the exchange */ - public ConsistentHashExchange(String name, boolean durable, boolean autoDelete, Map arguments) { + public ConsistentHashExchange(String name, boolean durable, boolean autoDelete, + @Nullable Map arguments) { + super(name, durable, autoDelete, arguments); - Assert.isTrue(!(arguments.containsKey("hash-header") && arguments.containsKey("hash-property")), + Assert.isTrue(arguments == null || + (!(arguments.containsKey("hash-header") && arguments.containsKey("hash-property"))), "The 'hash-header' and 'hash-property' are mutually exclusive."); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/CustomExchange.java b/spring-amqp/src/main/java/org/springframework/amqp/core/CustomExchange.java index 3d9e8f9cc1..3980470c64 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/CustomExchange.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/CustomExchange.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -18,11 +18,16 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + /** * Simple container collecting information to describe a custom exchange. Custom exchange types are allowed by the AMQP * specification, and their names should start with "x-" (but this is not enforced here). Used in conjunction with * administrative operations. + * * @author Dave Syer + * @author Artem Bilan + * * @see AmqpAdmin */ public class CustomExchange extends AbstractExchange { @@ -39,7 +44,9 @@ public CustomExchange(String name, String type, boolean durable, boolean autoDel this.type = type; } - public CustomExchange(String name, String type, boolean durable, boolean autoDelete, Map arguments) { + public CustomExchange(String name, String type, boolean durable, boolean autoDelete, + @Nullable Map arguments) { + super(name, durable, autoDelete, arguments); this.type = type; } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/Declarable.java b/spring-amqp/src/main/java/org/springframework/amqp/core/Declarable.java index b6948116fc..b2035cf7ba 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/Declarable.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/Declarable.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -18,7 +18,7 @@ import java.util.Collection; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Classes implementing this interface can be auto-declared @@ -26,14 +26,15 @@ * Registration can be limited to specific {@code AmqpAdmin}s. * * @author Gary Russell + * @author Artem Bilan + * * @since 1.2 * */ public interface Declarable { /** - * Whether or not this object should be automatically declared - * by any {@code AmqpAdmin}. + * Whether this object should be automatically declared by any {@code AmqpAdmin}. * @return true if the object should be declared. */ boolean shouldDeclare(); @@ -47,7 +48,7 @@ public interface Declarable { /** * Should ignore exceptions (such as mismatched args) when declaring. - * @return true if should ignore. + * @return true if it should ignore. * @since 1.6 */ boolean isIgnoreDeclarationExceptions(); diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/DirectExchange.java b/spring-amqp/src/main/java/org/springframework/amqp/core/DirectExchange.java index e7565ce819..dbbfb10bb4 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/DirectExchange.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/DirectExchange.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -18,11 +18,16 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + /** * Simple container collecting information to describe a direct exchange. * Used in conjunction with administrative operations. + * * @author Mark Pollack * @author Dave Syer + * @author Artem Bilan + * * @see AmqpAdmin */ public class DirectExchange extends AbstractExchange { @@ -41,7 +46,9 @@ public DirectExchange(String name, boolean durable, boolean autoDelete) { super(name, durable, autoDelete); } - public DirectExchange(String name, boolean durable, boolean autoDelete, Map arguments) { + public DirectExchange(String name, boolean durable, boolean autoDelete, + @Nullable Map arguments) { + super(name, durable, autoDelete, arguments); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/Exchange.java b/spring-amqp/src/main/java/org/springframework/amqp/core/Exchange.java index d99b9c98f9..b6ce157623 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/Exchange.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/Exchange.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -18,6 +18,8 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + /** * Interface for all exchanges. * @@ -61,6 +63,7 @@ public interface Exchange extends Declarable { * * @return the arguments. */ + @Nullable Map getArguments(); /** diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/FanoutExchange.java b/spring-amqp/src/main/java/org/springframework/amqp/core/FanoutExchange.java index 44ea3e0b89..8a2e137d1d 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/FanoutExchange.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/FanoutExchange.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -18,11 +18,16 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + /** * Simple container collecting information to describe a fanout exchange. * Used in conjunction with administrative operations. + * * @author Mark Pollack * @author Dave Syer + * @author Artem Bilan + * * @see AmqpAdmin */ public class FanoutExchange extends AbstractExchange { @@ -35,7 +40,9 @@ public FanoutExchange(String name, boolean durable, boolean autoDelete) { super(name, durable, autoDelete); } - public FanoutExchange(String name, boolean durable, boolean autoDelete, Map arguments) { + public FanoutExchange(String name, boolean durable, boolean autoDelete, + @Nullable Map arguments) { + super(name, durable, autoDelete, arguments); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/HeadersExchange.java b/spring-amqp/src/main/java/org/springframework/amqp/core/HeadersExchange.java index 892d9ab06f..4866175956 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/HeadersExchange.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/HeadersExchange.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -18,11 +18,14 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + /** * Headers exchange. * * @author Mark Fisher * @author Dave Syer + * @author Artem Bilan */ public class HeadersExchange extends AbstractExchange { @@ -34,7 +37,9 @@ public HeadersExchange(String name, boolean durable, boolean autoDelete) { super(name, durable, autoDelete); } - public HeadersExchange(String name, boolean durable, boolean autoDelete, Map arguments) { + public HeadersExchange(String name, boolean durable, boolean autoDelete, + @Nullable Map arguments) { + super(name, durable, autoDelete, arguments); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageBuilderSupport.java b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageBuilderSupport.java index 16ee6b32b9..e25fa459c0 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageBuilderSupport.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageBuilderSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 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. @@ -20,6 +20,8 @@ import java.util.Map; import java.util.Map.Entry; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; /** @@ -321,7 +323,7 @@ public MessageBuilderSupport copyHeaders(Map headers) { } public MessageBuilderSupport copyHeadersIfAbsent(Map headers) { - Map existingHeaders = this.properties.getHeaders(); + Map existingHeaders = this.properties.getHeaders(); for (Entry entry : headers.entrySet()) { if (!existingHeaders.containsKey(entry.getKey())) { existingHeaders.put(entry.getKey(), entry.getValue()); diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageDeliveryMode.java b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageDeliveryMode.java index 9c55e1813c..d65ca1111e 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageDeliveryMode.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageDeliveryMode.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -18,12 +18,13 @@ /** * Enumeration for the message delivery mode. Can be persistent or - * non persistent. Use the method 'toInt' to get the appropriate value + * non-persistent. Use the method 'toInt' to get the appropriate value * that is used by the AMQP protocol instead of the ordinal() value when * passing into AMQP APIs. * * @author Mark Pollack * @author Gary Russell + * @author Artem Bilan * */ public enum MessageDeliveryMode { @@ -39,25 +40,18 @@ public enum MessageDeliveryMode { PERSISTENT; public static int toInt(MessageDeliveryMode mode) { - switch (mode) { - case NON_PERSISTENT: - return 1; - case PERSISTENT: - return 2; - default: - return -1; - } + return switch (mode) { + case NON_PERSISTENT -> 1; + case PERSISTENT -> 2; + }; } public static MessageDeliveryMode fromInt(int modeAsNumber) { - switch (modeAsNumber) { - case 1: - return NON_PERSISTENT; - case 2: - return PERSISTENT; - default: - return null; - } + return switch (modeAsNumber) { + case 1 -> NON_PERSISTENT; + case 2 -> PERSISTENT; + default -> throw new IllegalArgumentException("Unknown mode: " + modeAsNumber); + }; } } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/MessagePostProcessor.java b/spring-amqp/src/main/java/org/springframework/amqp/core/MessagePostProcessor.java index 5cc27b8ba6..13f29019aa 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/MessagePostProcessor.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/MessagePostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 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. @@ -16,6 +16,8 @@ package org.springframework.amqp.core; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.AmqpException; /** @@ -56,7 +58,7 @@ public interface MessagePostProcessor { * @return the message. * @since 1.6.7 */ - default Message postProcessMessage(Message message, Correlation correlation) { + default Message postProcessMessage(Message message, @Nullable Correlation correlation) { return postProcessMessage(message); } @@ -70,7 +72,9 @@ default Message postProcessMessage(Message message, Correlation correlation) { * @return the message. * @since 2.3.4 */ - default Message postProcessMessage(Message message, Correlation correlation, String exchange, String routingKey) { + default Message postProcessMessage(Message message, @Nullable Correlation correlation, + String exchange, String routingKey) { + return postProcessMessage(message, correlation); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java index 81e4954d4c..758e03af70 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -16,6 +16,7 @@ package org.springframework.amqp.core; +import java.io.Serial; import java.io.Serializable; import java.lang.reflect.Method; import java.lang.reflect.Type; @@ -24,6 +25,8 @@ import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -40,8 +43,7 @@ */ public class MessageProperties implements Serializable { - private static final int INT_MASK = 32; - + @Serial private static final long serialVersionUID = 1619000546531112290L; public static final String CONTENT_TYPE_BYTES = "application/octet-stream"; @@ -78,66 +80,66 @@ public class MessageProperties implements Serializable { public static final Integer DEFAULT_PRIORITY = 0; /** - * The maximum value of x-delay header. - * @since 3.1.2 - */ + * The maximum value of x-delay header. + * @since 3.1.2 + */ public static final long X_DELAY_MAX = 0xffffffffL; - private final Map headers = new HashMap<>(); + private final HashMap headers = new HashMap<>(); - private Date timestamp; + private @Nullable Date timestamp; - private String messageId; + private @Nullable String messageId; - private String userId; + private @Nullable String userId; - private String appId; + private @Nullable String appId; - private String clusterId; + private @Nullable String clusterId; - private String type; + private @Nullable String type; - private String correlationId; + private @Nullable String correlationId; - private String replyTo; + private @Nullable String replyTo; private String contentType = DEFAULT_CONTENT_TYPE; - private String contentEncoding; + private @Nullable String contentEncoding; private long contentLength; private boolean contentLengthSet; - private MessageDeliveryMode deliveryMode = DEFAULT_DELIVERY_MODE; + private @Nullable MessageDeliveryMode deliveryMode = DEFAULT_DELIVERY_MODE; - private String expiration; + private @Nullable String expiration; private Integer priority = DEFAULT_PRIORITY; - private Boolean redelivered; + private @Nullable Boolean redelivered; - private String receivedExchange; + private @Nullable String receivedExchange; - private String receivedRoutingKey; + private @Nullable String receivedRoutingKey; - private String receivedUserId; + private @Nullable String receivedUserId; private long deliveryTag; private boolean deliveryTagSet; - private Integer messageCount; + private @Nullable Integer messageCount; // Not included in hashCode() - private String consumerTag; + private @Nullable String consumerTag; - private String consumerQueue; + private @Nullable String consumerQueue; - private Long receivedDelay; + private @Nullable Long receivedDelay; - private MessageDeliveryMode receivedDeliveryMode; + private @Nullable MessageDeliveryMode receivedDeliveryMode; private long retryCount; @@ -149,11 +151,11 @@ public class MessageProperties implements Serializable { private boolean projectionUsed; - private transient Type inferredArgumentType; + private transient @Nullable Type inferredArgumentType; - private transient Method targetMethod; + private transient @Nullable Method targetMethod; - private transient Object targetBean; + private transient @Nullable Object targetBean; public void setHeader(String key, Object value) { this.headers.put(key, value); @@ -176,11 +178,11 @@ public void setHeaders(Map headers) { * @since 2.2 */ @SuppressWarnings("unchecked") - public T getHeader(String headerName) { + public @Nullable T getHeader(String headerName) { return (T) this.headers.get(headerName); } - public Map getHeaders() { + public Map getHeaders() { return this.headers; } @@ -188,7 +190,7 @@ public void setTimestamp(Date timestamp) { this.timestamp = timestamp; //NOSONAR } - public Date getTimestamp() { + public @Nullable Date getTimestamp() { return this.timestamp; //NOSONAR } @@ -196,7 +198,7 @@ public void setMessageId(String messageId) { this.messageId = messageId; } - public String getMessageId() { + public @Nullable String getMessageId() { return this.messageId; } @@ -204,7 +206,7 @@ public void setUserId(String userId) { this.userId = userId; } - public String getUserId() { + public @Nullable String getUserId() { return this.userId; } @@ -213,7 +215,7 @@ public String getUserId() { * @return the user id. * @since 1.6 */ - public String getReceivedUserId() { + public @Nullable String getReceivedUserId() { return this.receivedUserId; } @@ -225,7 +227,7 @@ public void setAppId(String appId) { this.appId = appId; } - public String getAppId() { + public @Nullable String getAppId() { return this.appId; } @@ -233,7 +235,7 @@ public void setClusterId(String clusterId) { this.clusterId = clusterId; } - public String getClusterId() { + public @Nullable String getClusterId() { return this.clusterId; } @@ -241,7 +243,7 @@ public void setType(String type) { this.type = type; } - public String getType() { + public @Nullable String getType() { return this.type; } @@ -249,7 +251,7 @@ public String getType() { * Set the correlation id. * @param correlationId the id. */ - public void setCorrelationId(String correlationId) { + public void setCorrelationId(@Nullable String correlationId) { this.correlationId = correlationId; } @@ -257,23 +259,23 @@ public void setCorrelationId(String correlationId) { * Get the correlation id. * @return the id. */ - public String getCorrelationId() { + public @Nullable String getCorrelationId() { return this.correlationId; } - public void setReplyTo(String replyTo) { + public void setReplyTo(@Nullable String replyTo) { this.replyTo = replyTo; } - public String getReplyTo() { + public @Nullable String getReplyTo() { return this.replyTo; } - public void setReplyToAddress(Address replyTo) { + public void setReplyToAddress(@Nullable Address replyTo) { this.replyTo = (replyTo != null) ? replyTo.toString() : null; } - public Address getReplyToAddress() { + public @Nullable Address getReplyToAddress() { return (this.replyTo != null) ? new Address(this.replyTo) : null; } @@ -285,11 +287,11 @@ public String getContentType() { return this.contentType; } - public void setContentEncoding(String contentEncoding) { + public void setContentEncoding(@Nullable String contentEncoding) { this.contentEncoding = contentEncoding; } - public String getContentEncoding() { + public @Nullable String getContentEncoding() { return this.contentEncoding; } @@ -306,15 +308,15 @@ protected final boolean isContentLengthSet() { return this.contentLengthSet; } - public void setDeliveryMode(MessageDeliveryMode deliveryMode) { + public void setDeliveryMode(@Nullable MessageDeliveryMode deliveryMode) { this.deliveryMode = deliveryMode; } - public MessageDeliveryMode getDeliveryMode() { + public @Nullable MessageDeliveryMode getDeliveryMode() { return this.deliveryMode; } - public MessageDeliveryMode getReceivedDeliveryMode() { + public @Nullable MessageDeliveryMode getReceivedDeliveryMode() { return this.receivedDeliveryMode; } @@ -338,7 +340,7 @@ public void setExpiration(String expiration) { * milliseconds. * @return the expiration. */ - public String getExpiration() { + public @Nullable String getExpiration() { return this.expiration; } @@ -354,7 +356,7 @@ public void setReceivedExchange(String receivedExchange) { this.receivedExchange = receivedExchange; } - public String getReceivedExchange() { + public @Nullable String getReceivedExchange() { return this.receivedExchange; } @@ -362,7 +364,7 @@ public void setReceivedRoutingKey(String receivedRoutingKey) { this.receivedRoutingKey = receivedRoutingKey; } - public String getReceivedRoutingKey() { + public @Nullable String getReceivedRoutingKey() { return this.receivedRoutingKey; } @@ -373,7 +375,7 @@ public String getReceivedRoutingKey() { * @since 3.1.2 * @see #getDelayLong() */ - public Long getReceivedDelayLong() { + public @Nullable Long getReceivedDelayLong() { return this.receivedDelay; } @@ -392,14 +394,14 @@ public void setRedelivered(Boolean redelivered) { this.redelivered = redelivered; } - public Boolean isRedelivered() { + public @Nullable Boolean isRedelivered() { return this.redelivered; } /* * Additional accessor because is* is not standard for type Boolean */ - public Boolean getRedelivered() { + public @Nullable Boolean getRedelivered() { return this.redelivered; } @@ -430,11 +432,11 @@ public void setMessageCount(Integer messageCount) { * Only applies to messages retrieved via {@code basicGet}. * @return the count. */ - public Integer getMessageCount() { + public @Nullable Integer getMessageCount() { return this.messageCount; } - public String getConsumerTag() { + public @Nullable String getConsumerTag() { return this.consumerTag; } @@ -442,7 +444,7 @@ public void setConsumerTag(String consumerTag) { this.consumerTag = consumerTag; } - public String getConsumerQueue() { + public @Nullable String getConsumerQueue() { return this.consumerQueue; } @@ -455,7 +457,7 @@ public void setConsumerQueue(String consumerQueue) { * @return the delay. * @since 3.1.2 */ - public Long getDelayLong() { + public @Nullable Long getDelayLong() { Object delay = this.headers.get(X_DELAY); if (delay instanceof Long delayLong) { return delayLong; @@ -468,7 +470,7 @@ public Long getDelayLong() { * @param delay the delay. * @since 3.1.2 */ - public void setDelayLong(Long delay) { + public void setDelayLong(@Nullable Long delay) { if (delay == null || delay < 0) { this.headers.remove(X_DELAY); return; @@ -514,7 +516,7 @@ public void setFinalRetryForMessageWithNoId(boolean finalRetryForMessageWithNoId } /** - * Return the publish sequence number if publisher confirms are enabled; set by the template. + * Return the publishing sequence number if publisher confirms are enabled; set by the template. * @return the sequence number. * @since 2.1 */ @@ -523,7 +525,7 @@ public long getPublishSequenceNumber() { } /** - * Set the publish sequence number, if publisher confirms are enabled; set by the template. + * Set the publishing sequence number, if publisher confirms are enabled; set by the template. * @param publishSequenceNumber the sequence number. * @since 2.1 */ @@ -537,7 +539,7 @@ public void setPublishSequenceNumber(long publishSequenceNumber) { * @return the type. * @since 1.6 */ - public Type getInferredArgumentType() { + public @Nullable Type getInferredArgumentType() { return this.inferredArgumentType; } @@ -556,7 +558,7 @@ public void setInferredArgumentType(Type inferredArgumentType) { * @return the method. * @since 1.6 */ - public Method getTargetMethod() { + public @Nullable Method getTargetMethod() { return this.targetMethod; } @@ -565,7 +567,7 @@ public Method getTargetMethod() { * @param targetMethod the target method. * @since 1.6 */ - public void setTargetMethod(Method targetMethod) { + public void setTargetMethod(@Nullable Method targetMethod) { this.targetMethod = targetMethod; } @@ -574,7 +576,7 @@ public void setTargetMethod(Method targetMethod) { * @return the bean. * @since 1.6 */ - public Object getTargetBean() { + public @Nullable Object getTargetBean() { return this.targetBean; } @@ -583,7 +585,7 @@ public Object getTargetBean() { * @param targetBean the bean. * @since 1.6 */ - public void setTargetBean(Object targetBean) { + public void setTargetBean(@Nullable Object targetBean) { this.targetBean = targetBean; } @@ -630,7 +632,7 @@ public void setProjectionUsed(boolean projectionUsed) { * @return the header. */ @SuppressWarnings("unchecked") - public List> getXDeathHeader() { + public @Nullable List> getXDeathHeader() { try { return (List>) this.headers.get("x-death"); } @@ -646,16 +648,16 @@ public int hashCode() { result = prime * result + ((this.appId == null) ? 0 : this.appId.hashCode()); result = prime * result + ((this.clusterId == null) ? 0 : this.clusterId.hashCode()); result = prime * result + ((this.contentEncoding == null) ? 0 : this.contentEncoding.hashCode()); - result = prime * result + (int) (this.contentLength ^ (this.contentLength >>> INT_MASK)); - result = prime * result + ((this.contentType == null) ? 0 : this.contentType.hashCode()); + result = prime * result + Long.hashCode(this.contentLength); + result = prime * result + this.contentType.hashCode(); result = prime * result + ((this.correlationId == null) ? 0 : this.correlationId.hashCode()); result = prime * result + ((this.deliveryMode == null) ? 0 : this.deliveryMode.hashCode()); - result = prime * result + (int) (this.deliveryTag ^ (this.deliveryTag >>> INT_MASK)); + result = prime * result + Long.hashCode(this.deliveryTag); result = prime * result + ((this.expiration == null) ? 0 : this.expiration.hashCode()); result = prime * result + this.headers.hashCode(); result = prime * result + ((this.messageCount == null) ? 0 : this.messageCount.hashCode()); result = prime * result + ((this.messageId == null) ? 0 : this.messageId.hashCode()); - result = prime * result + ((this.priority == null) ? 0 : this.priority.hashCode()); + result = prime * result + this.priority.hashCode(); result = prime * result + ((this.receivedExchange == null) ? 0 : this.receivedExchange.hashCode()); result = prime * result + ((this.receivedRoutingKey == null) ? 0 : this.receivedRoutingKey.hashCode()); result = prime * result + ((this.redelivered == null) ? 0 : this.redelivered.hashCode()); @@ -705,12 +707,7 @@ else if (!this.contentEncoding.equals(other.contentEncoding)) { if (this.contentLength != other.contentLength) { return false; } - if (this.contentType == null) { - if (other.contentType != null) { - return false; - } - } - else if (!this.contentType.equals(other.contentType)) { + if (!this.contentType.equals(other.contentType)) { return false; } @@ -756,12 +753,7 @@ else if (!this.messageCount.equals(other.messageCount)) { else if (!this.messageId.equals(other.messageId)) { return false; } - if (this.priority == null) { - if (other.priority != null) { - return false; - } - } - else if (!this.priority.equals(other.priority)) { + if (!this.priority.equals(other.priority)) { return false; } if (this.receivedExchange == null) { @@ -830,13 +822,13 @@ public String toString() { + (this.type == null ? "" : ", type=" + this.type) + (this.correlationId == null ? "" : ", correlationId=" + this.correlationId) + (this.replyTo == null ? "" : ", replyTo=" + this.replyTo) - + (this.contentType == null ? "" : ", contentType=" + this.contentType) + + ", contentType=" + this.contentType + (this.contentEncoding == null ? "" : ", contentEncoding=" + this.contentEncoding) + ", contentLength=" + this.contentLength + (this.deliveryMode == null ? "" : ", deliveryMode=" + this.deliveryMode) + (this.receivedDeliveryMode == null ? "" : ", receivedDeliveryMode=" + this.receivedDeliveryMode) + (this.expiration == null ? "" : ", expiration=" + this.expiration) - + (this.priority == null ? "" : ", priority=" + this.priority) + + ", priority=" + this.priority + (this.redelivered == null ? "" : ", redelivered=" + this.redelivered) + (this.receivedExchange == null ? "" : ", receivedExchange=" + this.receivedExchange) + (this.receivedRoutingKey == null ? "" : ", receivedRoutingKey=" + this.receivedRoutingKey) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/Queue.java b/spring-amqp/src/main/java/org/springframework/amqp/core/Queue.java index 3ce08b21d9..a6fd51a101 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/Queue.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/Queue.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 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. @@ -19,7 +19,8 @@ import java.util.HashMap; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -28,6 +29,7 @@ * * @author Mark Pollack * @author Gary Russell + * * @see AmqpAdmin */ public class Queue extends AbstractDeclarable implements Cloneable { @@ -89,7 +91,7 @@ public Queue(String name, boolean durable, boolean exclusive, boolean autoDelete * @param arguments the arguments used to declare the queue */ public Queue(String name, boolean durable, boolean exclusive, boolean autoDelete, - @Nullable Map arguments) { + @Nullable Map arguments) { super(arguments); Assert.notNull(name, "'name' cannot be null"); diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/QueueBuilder.java b/spring-amqp/src/main/java/org/springframework/amqp/core/QueueBuilder.java index 37a622ff63..e2cc82c9af 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/QueueBuilder.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/QueueBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 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. @@ -226,8 +226,7 @@ public QueueBuilder lazy() { /** * Set the master locator mode which determines which node a queue master will be * located on a cluster of nodes. - * @param locator {@link MasterLocator#minMasters}, {@link MasterLocator#clientLocal} - * or {@link MasterLocator#random}. + * @param locator {@link LeaderLocator}. * @return the builder. * @since 2.2 */ diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/ReceiveAndReplyCallback.java b/spring-amqp/src/main/java/org/springframework/amqp/core/ReceiveAndReplyCallback.java index b3e11af821..0d8ba89cce 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/ReceiveAndReplyCallback.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/ReceiveAndReplyCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 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. @@ -16,22 +16,25 @@ package org.springframework.amqp.core; +import org.jspecify.annotations.Nullable; + /** * To be used with the receive-and-reply methods of {@link org.springframework.amqp.core.AmqpTemplate} * as processor for inbound object and producer for outbound object. * *

This often as an anonymous class within a method implementation. - * @param The type of the request after conversion from the {@link Message}. * @param The type of the response. * * @author Artem Bilan * @author Gary Russell + * * @since 1.3 */ @FunctionalInterface public interface ReceiveAndReplyCallback { + @Nullable S handle(R payload); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/TopicExchange.java b/spring-amqp/src/main/java/org/springframework/amqp/core/TopicExchange.java index fdac8366d9..114295e6d5 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/TopicExchange.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/TopicExchange.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -18,12 +18,16 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + /** * Simple container collecting information to describe a topic exchange. * Used in conjunction with administrative operations. * * @author Mark Pollack * @author Dave Syer + * @author Artem Bilan + * * @see AmqpAdmin */ public class TopicExchange extends AbstractExchange { @@ -36,7 +40,9 @@ public TopicExchange(String name, boolean durable, boolean autoDelete) { super(name, durable, autoDelete); } - public TopicExchange(String name, boolean durable, boolean autoDelete, Map arguments) { + public TopicExchange(String name, boolean durable, boolean autoDelete, + @Nullable Map arguments) { + super(name, durable, autoDelete, arguments); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/package-info.java b/spring-amqp/src/main/java/org/springframework/amqp/core/package-info.java index e04f829468..fea457ea01 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/package-info.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/package-info.java @@ -1,4 +1,5 @@ /** * Provides core classes for the spring AMQP abstraction. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.core; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/event/package-info.java b/spring-amqp/src/main/java/org/springframework/amqp/event/package-info.java index 5429f0253d..133bb99607 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/event/package-info.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/event/package-info.java @@ -1,4 +1,5 @@ /** * Classes related to application events */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.event; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/package-info.java b/spring-amqp/src/main/java/org/springframework/amqp/package-info.java index 0dfcd63dc9..9e2970f6d9 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/package-info.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/package-info.java @@ -1,4 +1,5 @@ /** * Base package for Spring AMQP. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/AmqpMessageHeaderAccessor.java b/spring-amqp/src/main/java/org/springframework/amqp/support/AmqpMessageHeaderAccessor.java index b6472364a1..05f9ae5a73 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/AmqpMessageHeaderAccessor.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/AmqpMessageHeaderAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 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. @@ -20,6 +20,8 @@ import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.MessageDeliveryMode; import org.springframework.messaging.Message; import org.springframework.messaging.support.NativeMessageHeaderAccessor; @@ -32,6 +34,7 @@ * * @author Stephane Nicoll * @author Gary Russell + * @author Artem Bilan * * @since 1.4 */ @@ -60,7 +63,8 @@ public static AmqpMessageHeaderAccessor wrap(Message message) { } @Override - protected void verifyType(String headerName, Object headerValue) { + @SuppressWarnings("NullAway") // Dataflow analysis limitation + protected void verifyType(@Nullable String headerName, @Nullable Object headerValue) { super.verifyType(headerName, headerValue); if (PRIORITY.equals(headerName)) { Assert.isTrue(Integer.class.isAssignableFrom(headerValue.getClass()), "The '" + headerName @@ -68,24 +72,24 @@ protected void verifyType(String headerName, Object headerValue) { } } - public String getAppId() { + public @Nullable String getAppId() { return (String) getHeader(AmqpHeaders.APP_ID); } - public String getClusterId() { + public @Nullable String getClusterId() { return (String) getHeader(AmqpHeaders.CLUSTER_ID); } - public String getContentEncoding() { + public @Nullable String getContentEncoding() { return (String) getHeader(AmqpHeaders.CONTENT_ENCODING); } - public Long getContentLength() { + public @Nullable Long getContentLength() { return (Long) getHeader(AmqpHeaders.CONTENT_LENGTH); } @Override - public MimeType getContentType() { + public @Nullable MimeType getContentType() { Object value = getHeader(AmqpHeaders.CONTENT_TYPE); if (value instanceof String contentType) { return MimeType.valueOf(contentType); @@ -93,60 +97,60 @@ public MimeType getContentType() { return super.getContentType(); } - public String getCorrelationId() { + public @Nullable String getCorrelationId() { return (String) getHeader(AmqpHeaders.CORRELATION_ID); } - public MessageDeliveryMode getDeliveryMode() { + public @Nullable MessageDeliveryMode getDeliveryMode() { return (MessageDeliveryMode) getHeader(AmqpHeaders.DELIVERY_MODE); } - public MessageDeliveryMode getReceivedDeliveryMode() { + public @Nullable MessageDeliveryMode getReceivedDeliveryMode() { return (MessageDeliveryMode) getHeader(AmqpHeaders.RECEIVED_DELIVERY_MODE); } - public Long getDeliveryTag() { + public @Nullable Long getDeliveryTag() { return (Long) getHeader(AmqpHeaders.DELIVERY_TAG); } - public String getExpiration() { + public @Nullable String getExpiration() { return (String) getHeader(AmqpHeaders.EXPIRATION); } - public Integer getMessageCount() { + public @Nullable Integer getMessageCount() { return (Integer) getHeader(AmqpHeaders.MESSAGE_COUNT); } - public String getMessageId() { + public @Nullable String getMessageId() { return (String) getHeader(AmqpHeaders.MESSAGE_ID); } - public Integer getPriority() { + public @Nullable Integer getPriority() { return (Integer) getHeader(PRIORITY); } - public String getReceivedExchange() { + public @Nullable String getReceivedExchange() { return (String) getHeader(AmqpHeaders.RECEIVED_EXCHANGE); } - public String getReceivedRoutingKey() { + public @Nullable String getReceivedRoutingKey() { return (String) getHeader(AmqpHeaders.RECEIVED_ROUTING_KEY); } - public String getReceivedUserId() { + public @Nullable String getReceivedUserId() { return (String) getHeader(AmqpHeaders.RECEIVED_USER_ID); } - public Boolean getRedelivered() { + public @Nullable Boolean getRedelivered() { return (Boolean) getHeader(AmqpHeaders.REDELIVERED); } - public String getReplyTo() { + public @Nullable String getReplyTo() { return (String) getHeader(AmqpHeaders.REPLY_TO); } @Override - public Long getTimestamp() { + public @Nullable Long getTimestamp() { Date amqpTimestamp = (Date) getHeader(AmqpHeaders.TIMESTAMP); if (amqpTimestamp != null) { return amqpTimestamp.getTime(); @@ -156,19 +160,19 @@ public Long getTimestamp() { } } - public String getType() { + public @Nullable String getType() { return (String) getHeader(AmqpHeaders.TYPE); } - public String getUserId() { + public @Nullable String getUserId() { return (String) getHeader(AmqpHeaders.USER_ID); } - public String getConsumerTag() { + public @Nullable String getConsumerTag() { return (String) getHeader(AmqpHeaders.CONSUMER_TAG); } - public String getConsumerQueue() { + public @Nullable String getConsumerQueue() { return (String) getHeader(AmqpHeaders.CONSUMER_QUEUE); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/ConditionalExceptionLogger.java b/spring-amqp/src/main/java/org/springframework/amqp/support/ConditionalExceptionLogger.java index 7d764fbada..26faaf721a 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/ConditionalExceptionLogger.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/ConditionalExceptionLogger.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2023 the original author or authors. + * Copyright 2015-2025 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. @@ -17,6 +17,7 @@ package org.springframework.amqp.support; import org.apache.commons.logging.Log; +import org.jspecify.annotations.Nullable; import org.springframework.core.log.LogMessage; @@ -37,7 +38,7 @@ public interface ConditionalExceptionLogger { * @param message a message that the caller suggests should be included in the log. * @param t a throwable; may be null. */ - void log(Log logger, String message, Throwable t); + void log(Log logger, String message, @Nullable Throwable t); /** * Log a consumer restart; debug by default. diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/SendRetryContextAccessor.java b/spring-amqp/src/main/java/org/springframework/amqp/support/SendRetryContextAccessor.java index 00542bd2cc..92ebdd0e0b 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/SendRetryContextAccessor.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/SendRetryContextAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2025 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. @@ -16,6 +16,8 @@ package org.springframework.amqp.support; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Address; import org.springframework.amqp.core.Message; import org.springframework.retry.RetryContext; @@ -48,7 +50,7 @@ private SendRetryContextAccessor() { * @return the message. * @see #MESSAGE */ - public static Message getMessage(RetryContext context) { + public static @Nullable Message getMessage(RetryContext context) { return (Message) context.getAttribute(MESSAGE); } @@ -58,7 +60,7 @@ public static Message getMessage(RetryContext context) { * @return the address. * @see #ADDRESS */ - public static Address getAddress(RetryContext context) { + public static @Nullable Address getAddress(RetryContext context) { return (Address) context.getAttribute(ADDRESS); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/SimpleAmqpHeaderMapper.java b/spring-amqp/src/main/java/org/springframework/amqp/support/SimpleAmqpHeaderMapper.java index 3f3f5bffbb..7865cc5388 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/SimpleAmqpHeaderMapper.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/SimpleAmqpHeaderMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -21,6 +21,8 @@ import java.util.Map; import java.util.function.BiConsumer; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.MessageDeliveryMode; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.utils.JavaUtils; @@ -116,11 +118,9 @@ public void fromHeaders(MessageHeaders headers, MessageProperties amqpMessagePro String headerName = entry.getKey(); if (StringUtils.hasText(headerName) && !headerName.startsWith(AmqpHeaders.PREFIX)) { Object value = entry.getValue(); - if (value != null) { - String propertyName = this.fromHeaderName(headerName); - if (!amqpMessageProperties.getHeaders().containsKey(headerName)) { - amqpMessageProperties.setHeader(propertyName, value); - } + String propertyName = this.fromHeaderName(headerName); + if (!amqpMessageProperties.getHeaders().containsKey(headerName)) { + amqpMessageProperties.setHeader(propertyName, value); } } } @@ -152,7 +152,7 @@ public MessageHeaders toHeaders(MessageProperties amqpMessageProperties) { .acceptIfNotNull(AmqpHeaders.MESSAGE_ID, amqpMessageProperties.getMessageId(), putObject); Integer priority = amqpMessageProperties.getPriority(); javaUtils - .acceptIfCondition(priority != null && priority > 0, AmqpMessageHeaderAccessor.PRIORITY, priority, + .acceptIfCondition(priority > 0, AmqpMessageHeaderAccessor.PRIORITY, priority, putObject) .acceptIfNotNull(AmqpHeaders.RECEIVED_DELAY, amqpMessageProperties.getReceivedDelayLong(), putObject) .acceptIfHasText(AmqpHeaders.RECEIVED_EXCHANGE, amqpMessageProperties.getReceivedExchange(), @@ -188,7 +188,7 @@ public MessageHeaders toHeaders(MessageProperties amqpMessageProperties) { * @param headers the headers. * @return the content type. */ - private String extractContentTypeAsString(Map headers) { + private @Nullable String extractContentTypeAsString(Map headers) { String contentTypeStringValue = null; Object contentType = getHeaderIfAvailable(headers, AmqpHeaders.CONTENT_TYPE, Object.class); diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java index 01c76a4c08..242e09446c 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java @@ -27,12 +27,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.MimeType; @@ -73,22 +73,21 @@ public abstract class AbstractJackson2MessageConverter extends AbstractMessageCo */ private MimeType supportedContentType; - private String supportedCTCharset; + private @Nullable String supportedCTCharset; - @Nullable - private ClassMapper classMapper = null; + private @Nullable ClassMapper classMapper = null; private Charset defaultCharset = DEFAULT_CHARSET; private boolean typeMapperSet; - private ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); private Jackson2JavaTypeMapper javaTypeMapper = new DefaultJackson2JavaTypeMapper(); private boolean useProjectionForInterfaces; - private ProjectingMessageConverter projectingConverter; + private @Nullable ProjectingMessageConverter projectingConverter; private boolean charsetIsUtf8 = true; @@ -192,7 +191,7 @@ public void setBeanClassLoader(ClassLoader classLoader) { } } - protected ClassLoader getClassLoader() { + protected @Nullable ClassLoader getClassLoader() { return this.classLoader; } @@ -201,7 +200,7 @@ public Jackson2JavaTypeMapper getJavaTypeMapper() { } /** - * Whether or not an explicit java type mapper has been provided. + * Whether an explicit java type mapper has been provided. * @return false if the default type mapper is being used. * @since 2.2 * @see #setJavaTypeMapper(Jackson2JavaTypeMapper) @@ -286,8 +285,8 @@ public void setUseProjectionForInterfaces(boolean useProjectionForInterfaces) { } /** - * By default the supported content type is assumed when there is no contentType - * property or it is set to the default ('application/octet-stream'). Set to 'false' + * By default, the supported content type is assumed when there is no contentType + * property, or it is set to the default ('application/octet-stream'). Set to 'false' * to revert to the previous behavior of returning an unconverted 'byte[]' when this * condition exists. * @param assumeSupportedContentType set false to not assume the content type is @@ -311,19 +310,18 @@ public Object fromMessage(Message message) throws MessageConversionException { public Object fromMessage(Message message, @Nullable Object conversionHint) throws MessageConversionException { Object content = null; MessageProperties properties = message.getMessageProperties(); - if (properties != null) { - String contentType = properties.getContentType(); - if ((this.assumeSupportedContentType // NOSONAR Boolean complexity - && (contentType == null || contentType.equals(MessageProperties.DEFAULT_CONTENT_TYPE))) - || (contentType != null && contentType.contains(this.supportedContentType.getSubtype()))) { - String encoding = determineEncoding(properties, contentType); - content = doFromMessage(message, conversionHint, properties, encoding); - } - else { - if (this.log.isWarnEnabled()) { - this.log.warn("Could not convert incoming message with content-type [" - + contentType + "], '" + this.supportedContentType.getSubtype() + "' keyword missing."); - } + String contentType = properties.getContentType(); + // NOSONAR Boolean complexity + if (this.assumeSupportedContentType && contentType.equals(MessageProperties.DEFAULT_CONTENT_TYPE) + || contentType.contains(this.supportedContentType.getSubtype())) { + + String encoding = determineEncoding(properties, contentType); + content = doFromMessage(message, conversionHint, properties, encoding); + } + else { + if (this.log.isWarnEnabled()) { + this.log.warn("Could not convert incoming message with content-type [" + + contentType + "], '" + this.supportedContentType.getSubtype() + "' keyword missing."); } } if (content == null) { @@ -342,11 +340,10 @@ private String determineEncoding(MessageProperties properties, @Nullable String if (encoding == null && contentType != null) { try { MimeType mimeType = MimeTypeUtils.parseMimeType(contentType); - if (mimeType != null) { - encoding = mimeType.getParameter("charset"); - } + encoding = mimeType.getParameter("charset"); } catch (RuntimeException e) { + // Ignore } } if (encoding == null) { @@ -355,7 +352,7 @@ private String determineEncoding(MessageProperties properties, @Nullable String return encoding; } - private Object doFromMessage(Message message, Object conversionHint, MessageProperties properties, + private Object doFromMessage(Message message, @Nullable Object conversionHint, MessageProperties properties, String encoding) { Object content = null; @@ -369,7 +366,8 @@ private Object doFromMessage(Message message, Object conversionHint, MessageProp return content; } - private Object convertContent(Message message, Object conversionHint, MessageProperties properties, String encoding) + @SuppressWarnings("NullAway") // Dataflow analysis limitation + private Object convertContent(Message message, @Nullable Object conversionHint, MessageProperties properties, String encoding) throws IOException { Object content = null; @@ -380,7 +378,7 @@ private Object convertContent(Message message, Object conversionHint, MessagePro properties.setProjectionUsed(true); } else if (inferredType != null && this.alwaysConvertToInferredType) { - content = tryConverType(message, encoding, inferredType); + content = tryConvertType(message, encoding, inferredType); } if (content == null) { if (conversionHint instanceof ParameterizedTypeReference parameterizedTypeReference) { @@ -408,8 +406,7 @@ else if (getClassMapper() == null) { * Unfortunately, mapper.canDeserialize() always returns true (adds an AbstractDeserializer * to the cache); so all we can do is try a conversion. */ - @Nullable - private Object tryConverType(Message message, String encoding, JavaType inferredType) { + private @Nullable Object tryConvertType(Message message, String encoding, JavaType inferredType) { try { return convertBytesToObject(message.getBody(), encoding, inferredType); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJavaTypeMapper.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJavaTypeMapper.java index 5aef390e2c..ddb881af72 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJavaTypeMapper.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJavaTypeMapper.java @@ -22,10 +22,10 @@ import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.type.TypeFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.MessageProperties; import org.springframework.beans.factory.BeanClassLoaderAware; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -36,6 +36,7 @@ * @author Andreas Asplund * @author Gary Russell * @author Ngoc Nhan + * @author Artem Bilan */ public abstract class AbstractJavaTypeMapper implements BeanClassLoaderAware { @@ -49,7 +50,7 @@ public abstract class AbstractJavaTypeMapper implements BeanClassLoaderAware { private final Map, String> classIdMapping = new HashMap<>(); - private ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); public String getClassIdFieldName() { return DEFAULT_CLASSID_FIELD_NAME; @@ -73,7 +74,7 @@ public void setBeanClassLoader(ClassLoader classLoader) { this.classLoader = classLoader; } - protected ClassLoader getClassLoader() { + protected @Nullable ClassLoader getClassLoader() { return this.classLoader; } @@ -95,9 +96,8 @@ protected String retrieveHeader(MessageProperties properties, String headerName) return classId; } - @Nullable - protected String retrieveHeaderAsString(MessageProperties properties, String headerName) { - Map headers = properties.getHeaders(); + protected @Nullable String retrieveHeaderAsString(MessageProperties properties, String headerName) { + Map headers = properties.getHeaders(); Object classIdFieldNameValue = headers.get(headerName); return classIdFieldNameValue != null ? classIdFieldNameValue.toString() diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractMessageConverter.java index d12a74bba3..f3e6d2c77e 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -19,9 +19,10 @@ import java.lang.reflect.Type; import java.util.UUID; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; -import org.springframework.lang.Nullable; /** * Convenient base class for {@link MessageConverter} implementations. diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverter.java index 35fb9d901c..0f6b9d10f3 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 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. @@ -34,6 +34,7 @@ * @author Gary Russell * @author Artem Bilan * @author Ngoc Nhan + * * @since 1.4.2 */ public class ContentTypeDelegatingMessageConverter implements MessageConverter { @@ -51,7 +52,7 @@ public ContentTypeDelegatingMessageConverter() { } /** - * Constructs an instance using a the supplied default converter. + * Constructs an instance using the supplied default converter. * May be null meaning a strict content-type match is required. * @param defaultConverter the converter. */ @@ -106,10 +107,6 @@ protected MessageConverter getConverterForContentType(String contentType) { delegate = this.defaultConverter; } - if (delegate == null) { - throw new MessageConversionException("No delegate converter is specified for content type " + contentType); - } - return delegate; } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultClassMapper.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultClassMapper.java index 3f5c9cf3cb..104bf49fec 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultClassMapper.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultClassMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -25,9 +25,10 @@ import java.util.Map.Entry; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.MessageProperties; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -60,9 +61,9 @@ public class DefaultClassMapper implements ClassMapper, InitializingBean { private final Set trustedPackages = new LinkedHashSet<>(TRUSTED_PACKAGES); - private volatile Map> idClassMapping = new HashMap<>(); + private final Map, String> classIdMapping = new HashMap<>(); - private volatile Map, String> classIdMapping = new HashMap<>(); + private volatile Map> idClassMapping = new HashMap<>(); private volatile Class defaultMapClass = LinkedHashMap.class; // NOSONAR concrete type @@ -117,7 +118,7 @@ public void setIdClassMapping(Map> idClassMapping) { * @param trustedPackages the trusted Java packages for deserialization * @since 1.6.11 */ - public void setTrustedPackages(@Nullable String... trustedPackages) { + public void setTrustedPackages(String @Nullable ... trustedPackages) { if (trustedPackages != null) { for (String trusted : trustedPackages) { if ("*".equals(trusted)) { @@ -169,22 +170,14 @@ public void fromClass(Class clazz, MessageProperties properties) { @Override public Class toClass(MessageProperties properties) { - Map headers = properties.getHeaders(); + Map headers = properties.getHeaders(); Object classIdFieldNameValue = headers.get(getClassIdFieldName()); String classId = null; if (classIdFieldNameValue != null) { classId = classIdFieldNameValue.toString(); } if (classId == null) { - if (this.defaultType != null) { - return this.defaultType; - } - else { - throw new MessageConversionException( - "failed to convert Message content. Could not resolve " - + getClassIdFieldName() + " in header " + - "and no defaultType provided"); - } + return this.defaultType; } return toClass(classId); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapper.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapper.java index fa97d5f2fb..c651c40c30 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapper.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapper.java @@ -23,9 +23,9 @@ import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.type.TypeFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.MessageProperties; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -91,7 +91,7 @@ public void setTypePrecedence(TypePrecedence typePrecedence) { * @param trustedPackages the trusted Java packages for deserialization * @since 1.6.11 */ - public void setTrustedPackages(@Nullable String... trustedPackages) { + public void setTrustedPackages(String @Nullable ... trustedPackages) { if (trustedPackages != null) { for (String trusted : trustedPackages) { if ("*".equals(trusted)) { @@ -106,7 +106,7 @@ public void setTrustedPackages(@Nullable String... trustedPackages) { } @Override - public void addTrustedPackages(@Nullable String... packages) { + public void addTrustedPackages(String @Nullable ... packages) { setTrustedPackages(packages); } @@ -137,10 +137,7 @@ private boolean canConvert(JavaType inferredType) { if (inferredType.isContainerType() && inferredType.getContentType().isAbstract()) { return false; } - if (inferredType.getKeyType() != null && inferredType.getKeyType().isAbstract()) { - return false; - } - return true; + return inferredType.getKeyType() == null || !inferredType.getKeyType().isAbstract(); } private JavaType fromTypeHeader(MessageProperties properties, String typeIdHeader) { @@ -161,8 +158,7 @@ private JavaType fromTypeHeader(MessageProperties properties, String typeIdHeade } @Override - @Nullable - public JavaType getInferredType(MessageProperties properties) { + public @Nullable JavaType getInferredType(MessageProperties properties) { if (this.typePrecedence.equals(TypePrecedence.INFERRED) && hasInferredTypeHeader(properties)) { return fromInferredTypeHeader(properties); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JavaTypeMapper.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JavaTypeMapper.java index 4f0391d8bb..ab7ef8737b 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JavaTypeMapper.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/Jackson2JavaTypeMapper.java @@ -17,9 +17,9 @@ package org.springframework.amqp.support.converter; import com.fasterxml.jackson.databind.JavaType; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.MessageProperties; -import org.springframework.lang.Nullable; /** * Strategy for setting metadata on messages such that one can create the class that needs diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MarshallingMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MarshallingMessageConverter.java index 6d94890221..0c0129042d 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MarshallingMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MarshallingMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -23,10 +23,11 @@ import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.oxm.Marshaller; import org.springframework.oxm.Unmarshaller; import org.springframework.oxm.XmlMappingException; @@ -45,12 +46,14 @@ * @see org.springframework.amqp.core.AmqpTemplate#receiveAndConvert() */ public class MarshallingMessageConverter extends AbstractMessageConverter implements InitializingBean { + + @SuppressWarnings("NullAway.Init") private volatile Marshaller marshaller; + @SuppressWarnings("NullAway.Init") private volatile Unmarshaller unmarshaller; - private volatile String contentType; - + private volatile @Nullable String contentType; /** * Construct a new MarshallingMessageConverter with no {@link Marshaller} or {@link Unmarshaller} set. @@ -75,8 +78,8 @@ public MarshallingMessageConverter(Marshaller marshaller) { if (!(marshaller instanceof Unmarshaller)) { throw new IllegalArgumentException( "Marshaller [" + marshaller + "] does not implement the Unmarshaller " + - "interface. Please set an Unmarshaller explicitly by using the " + - "MarshallingMessageConverter(Marshaller, Unmarshaller) constructor."); + "interface. Please set an Unmarshaller explicitly by using the " + + "MarshallingMessageConverter(Marshaller, Unmarshaller) constructor."); } this.marshaller = marshaller; @@ -96,7 +99,6 @@ public MarshallingMessageConverter(Marshaller marshaller, Unmarshaller unmarshal this.unmarshaller = unmarshaller; } - /** * Set the contentType to be used by this message converter. * @@ -132,7 +134,6 @@ public void afterPropertiesSet() { Assert.notNull(this.unmarshaller, "Property 'unmarshaller' is required"); } - /** * Marshals the given object to a {@link Message}. */ diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessageConverter.java index 77497e2718..fe857551b3 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -18,9 +18,10 @@ import java.lang.reflect.Type; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; -import org.springframework.lang.Nullable; /** * Message converter interface. @@ -53,7 +54,7 @@ public interface MessageConverter { default Message toMessage(Object object, MessageProperties messageProperties, @Nullable Type genericType) throws MessageConversionException { - return toMessage(object, messageProperties); + return toMessage(object, messageProperties); } /** diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessagingMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessagingMessageConverter.java index 5bc545086b..05a808b61c 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessagingMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessagingMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 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. @@ -125,9 +125,6 @@ public org.springframework.amqp.core.Message toMessage(Object object, MessagePro @SuppressWarnings("unchecked") @Override public Object fromMessage(org.springframework.amqp.core.Message message) throws MessageConversionException { - if (message == null) { - return null; - } Map mappedHeaders = this.headerMapper.toHeaders(message.getMessageProperties()); Object convertedObject = extractPayload(message); if (convertedObject == null) { diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationResult.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationResult.java index 6945162895..47d945ab3a 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationResult.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationResult.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 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. @@ -16,10 +16,11 @@ package org.springframework.amqp.support.converter; +import java.io.Serial; import java.io.Serializable; import java.lang.reflect.InvocationTargetException; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Encapsulates a remote invocation result, holding a result value or an exception. @@ -32,14 +33,13 @@ public class RemoteInvocationResult implements Serializable { /** Use serialVersionUID from Spring 1.1 for interoperability. */ + @Serial private static final long serialVersionUID = 2138555143707773549L; - @Nullable - private Object value; + private transient @Nullable Object value; - @Nullable - private Throwable exception; + private @Nullable Throwable exception; /** diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationUtils.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationUtils.java index 25aded8109..f8a8d8e385 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationUtils.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/RemoteInvocationUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2025 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. @@ -19,6 +19,8 @@ import java.util.HashSet; import java.util.Set; +import org.jspecify.annotations.Nullable; + /** * General utilities for handling remote invocations. * @@ -40,7 +42,7 @@ public abstract class RemoteInvocationUtils { * @see Throwable#getStackTrace() * @see Throwable#setStackTrace(StackTraceElement[]) */ - public static void fillInClientStackTraceIfPossible(Throwable ex) { + public static void fillInClientStackTraceIfPossible(@Nullable Throwable ex) { if (ex != null) { StackTraceElement[] clientStack = new Throwable().getStackTrace(); Set visitedExceptions = new HashSet<>(); diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SerializerMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SerializerMessageConverter.java index fdfc365c0e..4bd99684d4 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SerializerMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SerializerMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -24,6 +24,8 @@ import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; import org.springframework.beans.factory.BeanClassLoaderAware; @@ -32,7 +34,6 @@ import org.springframework.core.serializer.DefaultSerializer; import org.springframework.core.serializer.Deserializer; import org.springframework.core.serializer.Serializer; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -64,7 +65,7 @@ public class SerializerMessageConverter extends AllowedListDeserializingMessageC private boolean ignoreContentType = false; - private ClassLoader defaultDeserializerClassLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader defaultDeserializerClassLoader = ClassUtils.getDefaultClassLoader(); private boolean usingDefaultDeserializer = true; @@ -115,15 +116,12 @@ public void setBeanClassLoader(ClassLoader classLoader) { public Object fromMessage(Message message) throws MessageConversionException { Object content = null; MessageProperties properties = message.getMessageProperties(); - if (properties != null) { - String contentType = properties.getContentType(); - if (contentType != null && contentType.startsWith("text") && !this.ignoreContentType) { - content = asString(message, properties); - } - else if (contentType != null && contentType.equals(MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT) - || this.ignoreContentType) { - content = deserialize(message); - } + String contentType = properties.getContentType(); + if (contentType.startsWith("text") && !this.ignoreContentType) { + content = asString(message, properties); + } + else if (contentType.equals(MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT) || this.ignoreContentType) { + content = deserialize(message); } if (content == null) { content = message.getBody(); diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SimpleMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SimpleMessageConverter.java index 77ac469f21..f009bd85a4 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SimpleMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SimpleMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -24,12 +24,13 @@ import java.io.Serializable; import java.io.UnsupportedEncodingException; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.utils.SerializationUtils; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.core.ConfigurableObjectInputStream; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -50,7 +51,7 @@ public class SimpleMessageConverter extends AllowedListDeserializingMessageConve private String defaultCharset = DEFAULT_CHARSET; - private ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); /** * Specify the default charset to use when converting to or from text-based @@ -73,29 +74,26 @@ public void setBeanClassLoader(ClassLoader classLoader) { public Object fromMessage(Message message) throws MessageConversionException { Object content = null; MessageProperties properties = message.getMessageProperties(); - if (properties != null) { - String contentType = properties.getContentType(); - if (contentType != null && contentType.startsWith("text")) { - String encoding = properties.getContentEncoding(); - if (encoding == null) { - encoding = this.defaultCharset; - } - try { - content = new String(message.getBody(), encoding); - } - catch (UnsupportedEncodingException e) { - throw new MessageConversionException("failed to convert text-based Message content", e); - } + String contentType = properties.getContentType(); + if (contentType.startsWith("text")) { + String encoding = properties.getContentEncoding(); + if (encoding == null) { + encoding = this.defaultCharset; + } + try { + content = new String(message.getBody(), encoding); + } + catch (UnsupportedEncodingException e) { + throw new MessageConversionException("failed to convert text-based Message content", e); + } + } + else if (contentType.equals(MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT)) { + try { + content = SerializationUtils.deserialize( + createObjectInputStream(new ByteArrayInputStream(message.getBody()))); } - else if (contentType != null && - contentType.equals(MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT)) { - try { - content = SerializationUtils.deserialize( - createObjectInputStream(new ByteArrayInputStream(message.getBody()))); - } - catch (IOException | IllegalArgumentException | IllegalStateException e) { - throw new MessageConversionException("failed to convert serialized Message content", e); - } + catch (IOException | IllegalArgumentException | IllegalStateException e) { + throw new MessageConversionException("failed to convert serialized Message content", e); } } if (content == null) { diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SmartMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SmartMessageConverter.java index d3aca01c51..50a11ec9d7 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SmartMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/SmartMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 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. @@ -16,6 +16,8 @@ package org.springframework.amqp.support.converter; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Message; /** @@ -41,6 +43,6 @@ public interface SmartMessageConverter extends MessageConverter { * @throws MessageConversionException if the conversion fails. * @see #fromMessage(Message) */ - Object fromMessage(Message message, Object conversionHint) throws MessageConversionException; + Object fromMessage(Message message, @Nullable Object conversionHint) throws MessageConversionException; } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/package-info.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/package-info.java index ee1b21b087..47e4671c0d 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/package-info.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/package-info.java @@ -1,5 +1,5 @@ /** * Provides classes for supporting message conversion. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.amqp.support.converter; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/package-info.java b/spring-amqp/src/main/java/org/springframework/amqp/support/package-info.java index 997e2eb056..688dd25f14 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/package-info.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/package-info.java @@ -1,4 +1,5 @@ /** * Package for Spring AMQP support classes. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.support; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/AbstractCompressingPostProcessor.java b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/AbstractCompressingPostProcessor.java index 006975db2e..42b637df31 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/AbstractCompressingPostProcessor.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/AbstractCompressingPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * Copyright 2014-2025 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. @@ -80,8 +80,8 @@ public AbstractCompressingPostProcessor(boolean autoDecompress) { /** * Flag to indicate if {@link MessageProperties} should be used as is or cloned for new message * after compression. - * By default this flag is turned off for better performance since in most cases the original message - * is not used any more. + * By default, this flag is turned off for better performance since in most cases the original message + * is not used anymore. * @param copyProperties clone or reuse original message properties. * @since 2.1.5 */ diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/AbstractDecompressingPostProcessor.java b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/AbstractDecompressingPostProcessor.java index 3c87829cff..5d56d217f3 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/AbstractDecompressingPostProcessor.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/AbstractDecompressingPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 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. @@ -39,6 +39,8 @@ * * @author Gary Russell * @author Ngoc Nhan + * @author Artem Bilan + * * @since 1.4.2 */ public abstract class AbstractDecompressingPostProcessor implements MessagePostProcessor, Ordered { @@ -91,7 +93,9 @@ public Message postProcessMessage(Message message) throws AmqpException { ByteArrayOutputStream out = new ByteArrayOutputStream(); FileCopyUtils.copy(unzipper, out); MessageProperties messageProperties = message.getMessageProperties(); - String encoding = messageProperties.getContentEncoding(); + String contentEncoding = messageProperties.getContentEncoding(); + Assert.hasText(contentEncoding, "The 'encoding' message property is required"); + String encoding = contentEncoding; int delimAt = encoding.indexOf(':'); if (delimAt < 0) { delimAt = encoding.indexOf(','); @@ -105,9 +109,7 @@ public Message postProcessMessage(Message message) throws AmqpException { messageProperties.setContentEncoding(null); } else { - messageProperties.setContentEncoding(messageProperties.getContentEncoding() - .substring(delimAt + 1) - .trim()); + messageProperties.setContentEncoding(contentEncoding.substring(delimAt + 1).trim()); } messageProperties.getHeaders().remove(MessageProperties.SPRING_AUTO_DECOMPRESS); return new Message(out.toByteArray(), messageProperties); diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/DeflaterPostProcessor.java b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/DeflaterPostProcessor.java index 90ae7032ce..92cde7a474 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/DeflaterPostProcessor.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/DeflaterPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-2025 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. @@ -16,7 +16,6 @@ package org.springframework.amqp.support.postprocessor; -import java.io.IOException; import java.io.OutputStream; import java.util.zip.DeflaterOutputStream; @@ -38,7 +37,7 @@ public DeflaterPostProcessor(boolean autoDecompress) { } @Override - protected OutputStream getCompressorStream(OutputStream zipped) throws IOException { + protected OutputStream getCompressorStream(OutputStream zipped) { return new DeflaterPostProcessor.SettableLevelDeflaterOutputStream(zipped, getLevel()); } @@ -55,4 +54,5 @@ private static final class SettableLevelDeflaterOutputStream extends DeflaterOut } } + } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/InflaterPostProcessor.java b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/InflaterPostProcessor.java index 0d146642e7..c515dea410 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/InflaterPostProcessor.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/InflaterPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-2025 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. @@ -16,7 +16,6 @@ package org.springframework.amqp.support.postprocessor; -import java.io.IOException; import java.io.InputStream; import java.util.zip.InflaterInputStream; @@ -28,6 +27,7 @@ * @since 2.2 */ public class InflaterPostProcessor extends AbstractDecompressingPostProcessor { + public InflaterPostProcessor() { } @@ -35,11 +35,12 @@ public InflaterPostProcessor(boolean alwaysDecompress) { super(alwaysDecompress); } - protected InputStream getDecompressorStream(InputStream zipped) throws IOException { + protected InputStream getDecompressorStream(InputStream zipped) { return new InflaterInputStream(zipped); } protected String getEncoding() { return "deflate"; } + } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/MessagePostProcessorUtils.java b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/MessagePostProcessorUtils.java index 2c7a8f8b05..e69d83e444 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/MessagePostProcessorUtils.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/MessagePostProcessorUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 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. @@ -30,6 +30,8 @@ * * @author Gary Russell * @author Ngoc Nhan + * @author Artem Bilan + * * @since 1.4.2 * */ @@ -50,9 +52,8 @@ else if (processor instanceof Ordered) { unOrdered.add(processor); } } - List sorted = new ArrayList<>(); OrderComparator.sort(priorityOrdered); - sorted.addAll(priorityOrdered); + List sorted = new ArrayList<>(priorityOrdered); OrderComparator.sort(ordered); sorted.addAll(ordered); sorted.addAll(unOrdered); diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/package-info.java b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/package-info.java index 6644991861..e70ca88ce6 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/package-info.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/postprocessor/package-info.java @@ -1,4 +1,5 @@ /** * Package for Spring AMQP message post processors. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.support.postprocessor; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/utils/JavaUtils.java b/spring-amqp/src/main/java/org/springframework/amqp/utils/JavaUtils.java index e628e441b0..edd108e540 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/utils/JavaUtils.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/utils/JavaUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-2025 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. @@ -19,6 +19,8 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; + import org.springframework.util.StringUtils; /** @@ -26,6 +28,8 @@ * the singleton {@link #INSTANCE} and then chain calls to the utility methods. * * @author Gary Russell + * @author Artem Bilan + * * @since 2.1.4 * */ @@ -47,8 +51,8 @@ private JavaUtils() { * @param the value type. * @return this. */ - public JavaUtils acceptIfCondition(boolean condition, T value, Consumer consumer) { - if (condition) { + public JavaUtils acceptIfCondition(boolean condition, @Nullable T value, Consumer consumer) { + if (condition && value != null) { consumer.accept(value); } return this; @@ -61,7 +65,7 @@ public JavaUtils acceptIfCondition(boolean condition, T value, Consumer c * @param the value type. * @return this. */ - public JavaUtils acceptIfNotNull(T value, Consumer consumer) { + public JavaUtils acceptIfNotNull(@Nullable T value, Consumer consumer) { if (value != null) { consumer.accept(value); } @@ -74,7 +78,7 @@ public JavaUtils acceptIfNotNull(T value, Consumer consumer) { * @param consumer the consumer. * @return this. */ - public JavaUtils acceptIfHasText(String value, Consumer consumer) { + public JavaUtils acceptIfHasText(@Nullable String value, Consumer consumer) { if (StringUtils.hasText(value)) { consumer.accept(value); } @@ -109,7 +113,7 @@ public JavaUtils acceptIfCondition(boolean condition, T1 t1, T2 t2, BiC * @param the second argument type. * @return this. */ - public JavaUtils acceptIfNotNull(T1 t1, T2 t2, BiConsumer consumer) { + public JavaUtils acceptIfNotNull(T1 t1, @Nullable T2 t2, BiConsumer consumer) { if (t2 != null) { consumer.accept(t1, t2); } @@ -125,7 +129,7 @@ public JavaUtils acceptIfNotNull(T1 t1, T2 t2, BiConsumer consu * @param consumer the consumer. * @return this. */ - public JavaUtils acceptIfHasText(T t1, String value, BiConsumer consumer) { + public JavaUtils acceptIfHasText(T t1, @Nullable String value, BiConsumer consumer) { if (StringUtils.hasText(value)) { consumer.accept(t1, value); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/utils/SerializationUtils.java b/spring-amqp/src/main/java/org/springframework/amqp/utils/SerializationUtils.java index 6e9cb8de8c..131a10dfd1 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/utils/SerializationUtils.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/utils/SerializationUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2006-2023 the original author or authors. + * Copyright 2006-2025 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. @@ -25,6 +25,8 @@ import java.io.ObjectStreamClass; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.core.ConfigurableObjectInputStream; import org.springframework.util.ObjectUtils; import org.springframework.util.PatternMatchUtils; @@ -58,7 +60,7 @@ private SerializationUtils() { * @param object the object to serialize * @return an array of bytes representing the object in a portable fashion */ - public static byte[] serialize(Object object) { + public static byte @Nullable [] serialize(@Nullable Object object) { if (object == null) { return null; } @@ -77,7 +79,7 @@ public static byte[] serialize(Object object) { * @param bytes a serialized object created * @return the result of deserializing the bytes */ - public static Object deserialize(byte[] bytes) { + public static @Nullable Object deserialize(byte @Nullable [] bytes) { if (bytes == null) { return null; } @@ -94,7 +96,7 @@ public static Object deserialize(byte[] bytes) { * @param stream an object stream created from a serialized object * @return the result of deserializing the bytes */ - public static Object deserialize(ObjectInputStream stream) { + public static @Nullable Object deserialize(@Nullable ObjectInputStream stream) { if (stream == null) { return null; } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/utils/package-info.java b/spring-amqp/src/main/java/org/springframework/amqp/utils/package-info.java index 2a4a2ef163..08af0bf05b 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/utils/package-info.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/utils/package-info.java @@ -1,4 +1,5 @@ /** * Provides utility classes to support Spring AMQP. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.utils; diff --git a/spring-amqp/src/main/java/org/springframework/amqp/utils/test/TestUtils.java b/spring-amqp/src/main/java/org/springframework/amqp/utils/test/TestUtils.java index f8c802e558..a79991cecb 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/utils/test/TestUtils.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/utils/test/TestUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2024 the original author or authors. + * Copyright 2013-2025 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. @@ -16,6 +16,8 @@ package org.springframework.amqp.utils.test; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.DirectFieldAccessor; import org.springframework.util.Assert; @@ -40,7 +42,7 @@ private TestUtils() { * @param propertyPath The path. * @return The field. */ - public static Object getPropertyValue(Object root, String propertyPath) { + public static @Nullable Object getPropertyValue(Object root, String propertyPath) { Object value = null; DirectFieldAccessor accessor = new DirectFieldAccessor(root); String[] tokens = propertyPath.split("\\."); @@ -61,7 +63,7 @@ public static Object getPropertyValue(Object root, String propertyPath) { } @SuppressWarnings("unchecked") - public static T getPropertyValue(Object root, String propertyPath, Class type) { + public static @Nullable T getPropertyValue(Object root, String propertyPath, Class type) { Object value = getPropertyValue(root, propertyPath); if (value != null) { Assert.isAssignable(type, value.getClass()); diff --git a/spring-amqp/src/main/java/org/springframework/amqp/utils/test/package-info.java b/spring-amqp/src/main/java/org/springframework/amqp/utils/test/package-info.java new file mode 100644 index 0000000000..9b28026a84 --- /dev/null +++ b/spring-amqp/src/main/java/org/springframework/amqp/utils/test/package-info.java @@ -0,0 +1,5 @@ +/** + * Provides general testing utility classes. + */ +@org.jspecify.annotations.NullMarked +package org.springframework.amqp.utils.test; diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverterTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverterTests.java index 58598b0ff7..c76a30f052 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverterTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/ContentTypeDelegatingMessageConverterTests.java @@ -18,13 +18,14 @@ import java.io.Serializable; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; /** * @author Gary Russell @@ -35,6 +36,16 @@ */ public class ContentTypeDelegatingMessageConverterTests { + @BeforeAll + static void setUp() { + System.setProperty("spring.amqp.deserialization.trust.all", "true"); + } + + @AfterAll + static void tearDown() { + System.setProperty("spring.amqp.deserialization.trust.all", "false"); + } + @Test public void testDelegationOutbound() { ContentTypeDelegatingMessageConverter converter = new ContentTypeDelegatingMessageConverter(); @@ -56,16 +67,6 @@ public void testDelegationOutbound() { assertThat(new String(message.getBody())).isEqualTo("{\"foo\":\"bar\"}"); converted = converter.fromMessage(message); assertThat(converted).isInstanceOf(Foo.class); - - converter = new ContentTypeDelegatingMessageConverter(null); // no default - try { - converter.toMessage(foo, props); - fail("Expected exception"); - } - catch (Exception e) { - assertThat(e).isInstanceOf(MessageConversionException.class); - assertThat(e.getMessage()).contains("No delegate converter"); - } } @SuppressWarnings("serial") diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverterTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverterTests.java index 51e7d7ef2f..126b32ef9e 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverterTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverterTests.java @@ -73,7 +73,7 @@ public void before() { trade.setAccountName("Acct1"); trade.setBuyRequest(true); trade.setOrderType("Market"); - trade.setPrice(new BigDecimal(103.30)); + trade.setPrice(new BigDecimal("103.30")); trade.setQuantity(100); trade.setRequestId("R123"); trade.setTicker("VMW"); @@ -299,7 +299,6 @@ public void testMissingContentType() { Object foo = j2Converter.fromMessage(message); assertThat(foo).isInstanceOf(Foo.class); - messageProperties.setContentType(null); foo = j2Converter.fromMessage(message); assertThat(foo).isInstanceOf(Foo.class); @@ -670,6 +669,7 @@ public void setField(String field) { @SuppressWarnings("serial") public static class BazModule extends SimpleModule { + @SuppressWarnings("this-escape") public BazModule() { addDeserializer(Baz.class, new BazDeserializer()); } diff --git a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/MessagingMessageConverterTests.java b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/MessagingMessageConverterTests.java index 5bd63e1541..984bba3de2 100644 --- a/spring-amqp/src/test/java/org/springframework/amqp/support/converter/MessagingMessageConverterTests.java +++ b/spring-amqp/src/test/java/org/springframework/amqp/support/converter/MessagingMessageConverterTests.java @@ -49,11 +49,6 @@ public void toMessageWithTextMessage() { assertThat(new String(message.getBody())).isEqualTo("Hello World"); } - @Test - public void fromNull() { - assertThat(converter.fromMessage(null)).isNull(); - } - @Test public void customPayloadConverter() throws Exception { converter.setPayloadConverter(new SimpleMessageConverter() { diff --git a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/AbstractTestContainerTests.java b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/AbstractTestContainerTests.java index 0d45fc7aec..215e07a593 100644 --- a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/AbstractTestContainerTests.java +++ b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/AbstractTestContainerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors. + * Copyright 2021-2025 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. @@ -19,6 +19,9 @@ import java.io.IOException; import java.time.Duration; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeAll; import org.testcontainers.containers.RabbitMQContainer; import org.testcontainers.junit.jupiter.Testcontainers; @@ -34,7 +37,9 @@ @Testcontainers(disabledWithoutDocker = true) public abstract class AbstractTestContainerTests { - protected static final RabbitMQContainer RABBITMQ; + private static final Log LOG = LogFactory.getLog(AbstractTestContainerTests.class); + + protected static final @Nullable RabbitMQContainer RABBITMQ; static { if (System.getProperty("spring.rabbit.use.local.server") == null @@ -57,8 +62,13 @@ public abstract class AbstractTestContainerTests { @BeforeAll static void startContainer() throws IOException, InterruptedException { - RABBITMQ.start(); - RABBITMQ.execInContainer("rabbitmq-plugins", "enable", "rabbitmq_stream"); + if (RABBITMQ != null) { + RABBITMQ.start(); + RABBITMQ.execInContainer("rabbitmq-plugins", "enable", "rabbitmq_stream"); + } + else { + LOG.info("The local RabbitMQ broker will be used instead of Testcontainers."); + } } public static int amqpPort() { @@ -74,7 +84,7 @@ public static int streamPort() { } public static String restUri() { - return RABBITMQ.getHttpUrl() + "/api/"; + return RABBITMQ != null ? RABBITMQ.getHttpUrl() + "/api/" : "http://localhost:" + managementPort() + "/api/"; } } diff --git a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java index 3fdce94135..54f4c5d94d 100644 --- a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java +++ b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/BrokerRunningSupport.java @@ -17,6 +17,7 @@ package org.springframework.amqp.rabbit.junit; import java.io.IOException; +import java.io.Serial; import java.net.Authenticator; import java.net.PasswordAuthentication; import java.net.URI; @@ -41,6 +42,7 @@ import com.rabbitmq.client.ConnectionFactory; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.http.HttpStatus; import org.springframework.util.StringUtils; @@ -58,6 +60,7 @@ * * @author Dave Syer * @author Gary Russell + * @author Artem Bilan * * @since 2.2 */ @@ -96,15 +99,15 @@ public final class BrokerRunningSupport { private final boolean management; - private final String[] queues; + private final String @Nullable [] queues; private int port; private String hostName = fromEnvironment(BROKER_HOSTNAME, "localhost"); - private String adminUri = fromEnvironment(BROKER_ADMIN_URI, null); + private @Nullable String adminUri = fromEnvironment(BROKER_ADMIN_URI); - private ConnectionFactory connectionFactory; + private @Nullable ConnectionFactory connectionFactory; private String user = fromEnvironment(BROKER_USER, GUEST); @@ -116,6 +119,19 @@ public final class BrokerRunningSupport { private boolean purgeAfterEach; + private static @Nullable String fromEnvironment(String key) { + String environmentValue = ENVIRONMENT_OVERRIDES.get(key); + if (!StringUtils.hasText(environmentValue)) { + environmentValue = System.getenv(key); + } + if (StringUtils.hasText(environmentValue)) { + return environmentValue; + } + else { + return null; + } + } + private static String fromEnvironment(String key, String defaultValue) { String environmentValue = ENVIRONMENT_OVERRIDES.get(key); if (!StringUtils.hasText(environmentValue)) { @@ -151,7 +167,6 @@ public static void clearEnvironmentVariableOverrides() { /** * Ensure the broker is running and has a empty queue(s) with the specified name(s) in the * default exchange. - * * @param names the queues to declare for the test. * @return a new rule that assumes an existing running broker */ @@ -193,7 +208,7 @@ private BrokerRunningSupport(boolean purge, String... queues) { this(purge, false, queues); } - BrokerRunningSupport(boolean purge, boolean management, String... queues) { + BrokerRunningSupport(boolean purge, boolean management, String @Nullable ... queues) { if (queues != null) { this.queues = Arrays.copyOf(queues, queues.length); } @@ -202,9 +217,10 @@ private BrokerRunningSupport(boolean purge, String... queues) { } this.purge = purge; this.management = management; - setPort(fromEnvironment(BROKER_PORT, null) == null + String portFromEnvironment = fromEnvironment(BROKER_PORT); + setPort(portFromEnvironment == null ? BrokerTestUtils.getPort() - : Integer.valueOf(fromEnvironment(BROKER_PORT, null))); + : Integer.parseInt(portFromEnvironment)); } private BrokerRunningSupport(boolean assumeOnline) { @@ -313,7 +329,6 @@ public String getAdminPassword() { return this.adminPassword; } - public boolean isPurgeAfterEach() { return this.purgeAfterEach; } @@ -363,22 +378,24 @@ private Channel createQueues(Connection connection) throws IOException, URISynta Channel channel; channel = connection.createChannel(); - for (String queueName : this.queues) { - - if (this.purge) { - LOGGER.debug("Deleting queue: " + queueName); - // Delete completely - gets rid of consumers and bindings as well - channel.queueDelete(queueName); - } - - if (isDefaultQueue(queueName)) { - // Just for test probe. - channel.queueDelete(queueName); - } - else { - channel.queueDeclare(queueName, true, false, false, null); + if (this.queues != null) { + for (String queueName : this.queues) { + if (this.purge) { + LOGGER.debug("Deleting queue: " + queueName); + // Delete completely - gets rid of consumers and bindings as well + channel.queueDelete(queueName); + } + + if (isDefaultQueue(queueName)) { + // Just for test probe. + channel.queueDelete(queueName); + } + else { + channel.queueDeclare(queueName, true, false, false, null); + } } } + if (this.management) { alivenessTest(); } @@ -418,7 +435,7 @@ protected PasswordAuthentication getPasswordAuthentication() { body = response.body(); } if (body == null || !body.contentEquals("{\"status\":\"ok\"}")) { - throw new BrokerNotAliveException("Aliveness test failed for " + uri.toString() + throw new BrokerNotAliveException("Aliveness test failed for " + uri + " user: " + getAdminUser() + " pw: " + getAdminPassword() + " status: " + response.statusCode() + " body: " + body + "; management not available"); @@ -445,7 +462,7 @@ public String generateId() { UUID uuid = UUID.randomUUID(); ByteBuffer bb = ByteBuffer.wrap(new byte[SIXTEEN]); bb.putLong(uuid.getMostSignificantBits()) - .putLong(uuid.getLeastSignificantBits()); + .putLong(uuid.getLeastSignificantBits()); return "SpringBrokerRunning." + Base64.getUrlEncoder().encodeToString(bb.array()).replaceAll("=", ""); } @@ -459,8 +476,8 @@ private boolean isDefaultQueue(String queue) { * @param additionalQueues additional queues to remove that might have been created by * tests. */ - public void removeTestQueues(String... additionalQueues) { - List queuesToRemove = Arrays.asList(this.queues); + public void removeTestQueues(String @Nullable ... additionalQueues) { + List queuesToRemove = this.queues != null ? Arrays.asList(this.queues) : new ArrayList<>(); if (additionalQueues != null) { queuesToRemove = new ArrayList<>(queuesToRemove); queuesToRemove.addAll(Arrays.asList(additionalQueues)); @@ -620,7 +637,7 @@ public String getAdminUri() { return this.adminUri; } - private void closeResources(Connection connection, Channel channel) { + private void closeResources(@Nullable Connection connection, @Nullable Channel channel) { if (channel != null) { try { channel.close(); @@ -645,6 +662,7 @@ private void closeResources(Connection connection, Channel channel) { */ public static class BrokerNotAliveException extends RuntimeException { + @Serial private static final long serialVersionUID = 1L; BrokerNotAliveException(String message) { diff --git a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/JUnitUtils.java b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/JUnitUtils.java index 6eedd301e3..35706b0dc3 100644 --- a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/JUnitUtils.java +++ b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/JUnitUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 the original author or authors. + * Copyright 2019-2025 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. @@ -19,7 +19,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -52,7 +51,7 @@ private JUnitUtils() { * @return the parsed property value if it exists, false otherwise. */ public static boolean parseBooleanProperty(String property) { - for (String value : new String[] { System.getenv(property), System.getProperty(property) }) { + for (String value : new String[] {System.getenv(property), System.getProperty(property)}) { if (Boolean.parseBoolean(value)) { return true; } @@ -118,8 +117,8 @@ public static LevelsContainer adjustLogLevels(String methodName, List> + "Overridden log level setting for: " + classes.stream() .map(Class::getSimpleName) - .collect(Collectors.toList()) - + " and " + categories.toString() + .toList() + + " and " + categories + " for test " + methodName); return new LevelsContainer(classLevels, categoryLevels, oldLbLevels); } diff --git a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/package-info.java b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/package-info.java index a57318ae9b..ed6deb88c2 100644 --- a/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/package-info.java +++ b/spring-rabbit-junit/src/main/java/org/springframework/amqp/rabbit/junit/package-info.java @@ -2,4 +2,5 @@ * Provides support classes (Rules etc. with no spring-rabbit dependencies) for JUnit * tests. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.junit; diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/StreamRabbitListenerContainerFactory.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/StreamRabbitListenerContainerFactory.java index 6911571179..5e64318ec6 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/StreamRabbitListenerContainerFactory.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/StreamRabbitListenerContainerFactory.java @@ -20,6 +20,7 @@ import com.rabbitmq.stream.Environment; import org.aopalliance.aop.Advice; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.rabbit.batch.BatchingStrategy; import org.springframework.amqp.rabbit.config.BaseRabbitListenerContainerFactory; @@ -28,7 +29,6 @@ import org.springframework.amqp.rabbit.listener.RabbitListenerEndpoint; import org.springframework.amqp.rabbit.listener.api.RabbitListenerErrorHandler; import org.springframework.amqp.utils.JavaUtils; -import org.springframework.lang.Nullable; import org.springframework.rabbit.stream.listener.ConsumerCustomizer; import org.springframework.rabbit.stream.listener.StreamListenerContainer; import org.springframework.rabbit.stream.listener.adapter.StreamMessageListenerAdapter; @@ -49,11 +49,11 @@ public class StreamRabbitListenerContainerFactory private boolean nativeListener; - private ConsumerCustomizer consumerCustomizer; + private @Nullable ConsumerCustomizer consumerCustomizer; - private ContainerCustomizer containerCustomizer; + private @Nullable ContainerCustomizer containerCustomizer; - private RabbitStreamListenerObservationConvention streamListenerObservationConvention; + private @Nullable RabbitStreamListenerObservationConvention streamListenerObservationConvention; /** * Construct an instance using the provided environment. @@ -102,11 +102,12 @@ public void setStreamListenerObservationConvention( } @Override - public StreamListenerContainer createListenerContainer(RabbitListenerEndpoint endpoint) { + public StreamListenerContainer createListenerContainer(@Nullable RabbitListenerEndpoint endpoint) { if (endpoint instanceof MethodRabbitListenerEndpoint methodRabbitListenerEndpoint && this.nativeListener) { methodRabbitListenerEndpoint.setAdapterProvider( - (boolean batch, Object bean, Method method, boolean returnExceptions, - RabbitListenerErrorHandler errorHandler, @Nullable BatchingStrategy batchingStrategy) -> { + (boolean batch, @Nullable Object bean, @Nullable Method method, boolean returnExceptions, + @Nullable RabbitListenerErrorHandler errorHandler, + @Nullable BatchingStrategy batchingStrategy) -> { Assert.isTrue(!batch, "Batch listeners are not supported by the stream container"); return new StreamMessageListenerAdapter(bean, method, returnExceptions, errorHandler); diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStream.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStream.java index 6523ee8814..52df70552f 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStream.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStream.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors. + * Copyright 2022-2025 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. @@ -25,6 +25,8 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.Binding.DestinationType; import org.springframework.amqp.core.Declarable; @@ -38,6 +40,8 @@ * * @author Gary Russell * @author Sergei Kurenchuk + * @author Artem Bilan + * * @since 3.0 */ public class SuperStream extends Declarables { @@ -60,8 +64,8 @@ public SuperStream(String name, int partitions) { */ public SuperStream(String name, int partitions, Map arguments) { this(name, partitions, (q, i) -> IntStream.range(0, i) - .mapToObj(String::valueOf) - .collect(Collectors.toList()), + .mapToObj(String::valueOf) + .collect(Collectors.toList()), arguments ); } @@ -88,27 +92,30 @@ public SuperStream(String name, int partitions, BiFunction> routingKeyStrategy, Map arguments) { + public SuperStream(String name, int partitions, BiFunction> routingKeyStrategy, + Map arguments) { + super(declarables(name, partitions, routingKeyStrategy, arguments)); } private static Collection declarables(String name, int partitions, - BiFunction> routingKeyStrategy, - Map arguments) { + BiFunction> routingKeyStrategy, + Map arguments) { List declarables = new ArrayList<>(); List rks = routingKeyStrategy.apply(name, partitions); Assert.state(rks.size() == partitions, () -> "Expected " + partitions + " routing keys, not " + rks.size()); - declarables.add(new DirectExchange(name, true, false, Map.of("x-super-stream", true))); + declarables.add( + new DirectExchange(name, true, false, Map.of("x-super-stream", true))); - Map argumentsCopy = new HashMap<>(arguments); + Map argumentsCopy = new HashMap<>(arguments); argumentsCopy.put("x-queue-type", "stream"); for (int i = 0; i < partitions; i++) { String rk = rks.get(i); Queue q = new Queue(name + "-" + i, true, false, false, argumentsCopy); declarables.add(q); declarables.add(new Binding(q.getName(), DestinationType.QUEUE, name, rk, - Map.of("x-stream-partition-order", i))); + Map.of("x-stream-partition-order", i))); } return declarables; } diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStreamBuilder.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStreamBuilder.java index cbe565e80a..82175b5914 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStreamBuilder.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/SuperStreamBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors. + * Copyright 2021-2025 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. @@ -21,6 +21,8 @@ import java.util.Map; import java.util.function.BiFunction; +import org.jspecify.annotations.Nullable; + import org.springframework.util.StringUtils; /** @@ -32,11 +34,14 @@ * @since 3.1 */ public class SuperStreamBuilder { + private final Map arguments = new HashMap<>(); - private String name; + + private @Nullable String name; + private int partitions = -1; - private BiFunction> routingKeyStrategy; + private @Nullable BiFunction> routingKeyStrategy; /** * Creates a builder for Super Stream. @@ -163,4 +168,5 @@ public SuperStream build() { return new SuperStream(this.name, this.partitions, this.routingKeyStrategy, this.arguments); } + } diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/package-info.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/package-info.java index 7b55932b3d..fb1cebfbc2 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/package-info.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/config/package-info.java @@ -1,5 +1,5 @@ /** * Provides classes for stream listener configuration. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.rabbit.stream.config; diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java index a4d3706719..44b1ece514 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java @@ -30,6 +30,7 @@ import io.micrometer.observation.ObservationRegistry; import org.aopalliance.aop.Advice; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageListener; @@ -40,7 +41,6 @@ import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.core.log.LogAccessor; -import org.springframework.lang.Nullable; import org.springframework.rabbit.stream.micrometer.RabbitStreamListenerObservation; import org.springframework.rabbit.stream.micrometer.RabbitStreamListenerObservation.DefaultRabbitStreamListenerObservationConvention; import org.springframework.rabbit.stream.micrometer.RabbitStreamListenerObservationConvention; @@ -56,6 +56,8 @@ * @author Gary Russell * @author Christian Tzolov * @author Ngoc Nhan + * @author Artem Bilan + * * @since 2.4 * */ @@ -71,7 +73,8 @@ public class StreamListenerContainer extends ObservableListenerContainer { private StreamMessageConverter streamConverter; - private ConsumerCustomizer consumerCustomizer = (id, con) -> { }; + private ConsumerCustomizer consumerCustomizer = (id, con) -> { + }; private boolean simpleStream; @@ -81,16 +84,16 @@ public class StreamListenerContainer extends ObservableListenerContainer { private boolean autoStartup = true; - private MessageListener messageListener; + private @Nullable MessageListener messageListener; - private StreamMessageListener streamListener; + private @Nullable StreamMessageListener streamListener; - private Advice[] adviceChain; + private Advice @Nullable [] adviceChain; + @SuppressWarnings("NullAway.Init") private String streamName; - @Nullable - private RabbitStreamListenerObservationConvention observationConvention; + private @Nullable RabbitStreamListenerObservationConvention observationConvention; /** * Construct an instance using the provided environment. @@ -108,7 +111,10 @@ public StreamListenerContainer(Environment environment) { public StreamListenerContainer(Environment environment, @Nullable Codec codec) { Assert.notNull(environment, "'environment' cannot be null"); this.builder = environment.consumerBuilder(); - this.streamConverter = new DefaultStreamMessageConverter(codec); + this.streamConverter = + codec != null + ? new DefaultStreamMessageConverter(codec) + : new DefaultStreamMessageConverter(); } /** @@ -116,7 +122,7 @@ public StreamListenerContainer(Environment environment, @Nullable Codec codec) { * Mutually exclusive with {@link #superStream(String, String)}. */ @Override - public void setQueueNames(String... queueNames) { + public void setQueueNames(String @Nullable ... queueNames) { Assert.isTrue(!this.superStream, "setQueueNames() and superStream() are mutually exclusive"); Assert.isTrue(queueNames != null && queueNames.length == 1, "Only one stream is supported"); this.lock.lock(); @@ -226,8 +232,7 @@ public void setAdviceChain(Advice... advices) { } @Override - @Nullable - public Object getMessageListener() { + public @Nullable Object getMessageListener() { return this.messageListener; } @@ -314,22 +319,23 @@ public void setupMessageListener(MessageListener messageListener) { () -> new RabbitStreamMessageReceiverContext(message, getListenerId(), this.streamName), registry); Object finalSample = sample; - if (this.streamListener != null) { + StreamMessageListener streamListenerToUse = this.streamListener; + if (streamListenerToUse != null) { observation.observe(() -> { try { - this.streamListener.onStreamMessage(message, context); - if (finalSample != null) { + streamListenerToUse.onStreamMessage(message, context); + if (micrometerHolder != null && finalSample != null) { micrometerHolder.success(finalSample, this.streamName); } } catch (RuntimeException rtex) { - if (finalSample != null) { + if (micrometerHolder != null && finalSample != null) { micrometerHolder.failure(finalSample, this.streamName, rtex.getClass().getSimpleName()); } throw rtex; } catch (Exception ex) { - if (finalSample != null) { + if (micrometerHolder != null && finalSample != null) { micrometerHolder.failure(finalSample, this.streamName, ex.getClass().getSimpleName()); } throw RabbitExceptionTranslator.convertRabbitAccessException(ex); @@ -343,19 +349,19 @@ public void setupMessageListener(MessageListener messageListener) { observation.observe(() -> { try { channelAwareMessageListener.onMessage(message2, null); - if (finalSample != null) { + if (micrometerHolder != null && finalSample != null) { micrometerHolder.success(finalSample, this.streamName); } } catch (RuntimeException rtex) { - if (finalSample != null) { + if (micrometerHolder != null && finalSample != null) { micrometerHolder.failure(finalSample, this.streamName, rtex.getClass().getSimpleName()); } throw rtex; } catch (Exception ex) { - if (finalSample != null) { + if (micrometerHolder != null && finalSample != null) { micrometerHolder.failure(finalSample, this.streamName, ex.getClass().getSimpleName()); } throw RabbitExceptionTranslator.convertRabbitAccessException(ex); @@ -367,7 +373,9 @@ public void setupMessageListener(MessageListener messageListener) { } } else { - observation.observe(() -> this.messageListener.onMessage(message2)); + MessageListener messageListenerToUse = this.messageListener; + Assert.state(messageListenerToUse != null, "'messageListener' or 'streamListener' is required"); + observation.observe(() -> messageListenerToUse.onMessage(message2)); } } }); diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/adapter/StreamMessageListenerAdapter.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/adapter/StreamMessageListenerAdapter.java index 30748291b6..870d5d3050 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/adapter/StreamMessageListenerAdapter.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/adapter/StreamMessageListenerAdapter.java @@ -20,11 +20,13 @@ import com.rabbitmq.stream.Message; import com.rabbitmq.stream.MessageHandler.Context; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.rabbit.listener.adapter.InvocationResult; import org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter; import org.springframework.amqp.rabbit.listener.api.RabbitListenerErrorHandler; import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; +import org.springframework.messaging.support.GenericMessage; import org.springframework.rabbit.stream.listener.StreamMessageListener; /** @@ -36,6 +38,11 @@ */ public class StreamMessageListenerAdapter extends MessagingMessageListenerAdapter implements StreamMessageListener { + /** + * The {@code org.springframework.messaging.handler.invocation.InvocableHandlerMethod} contact support. + */ + private static final GenericMessage FAKE_MESSAGE = new GenericMessage<>(""); + /** * Construct an instance with the provided arguments. * @param bean the bean. @@ -43,8 +50,8 @@ public class StreamMessageListenerAdapter extends MessagingMessageListenerAdapte * @param returnExceptions true to return exceptions. * @param errorHandler the error handler. */ - public StreamMessageListenerAdapter(Object bean, Method method, boolean returnExceptions, - RabbitListenerErrorHandler errorHandler) { + public StreamMessageListenerAdapter(@Nullable Object bean, @Nullable Method method, boolean returnExceptions, + @Nullable RabbitListenerErrorHandler errorHandler) { super(bean, method, returnExceptions, errorHandler); } @@ -52,7 +59,7 @@ public StreamMessageListenerAdapter(Object bean, Method method, boolean returnEx @Override public void onStreamMessage(Message message, Context context) { try { - InvocationResult result = getHandlerAdapter().invoke(null, message, context); + InvocationResult result = getHandlerAdapter().invoke(FAKE_MESSAGE, message, context); if (result.getReturnValue() != null) { logger.warn("Replies are not currently supported with native Stream listeners"); } diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/adapter/package-info.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/adapter/package-info.java index 80f24d55c5..cc90755ae2 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/adapter/package-info.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/adapter/package-info.java @@ -1,5 +1,5 @@ /** * Provides classes for stream listener adapters. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.rabbit.stream.listener.adapter; diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/package-info.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/package-info.java index 64517ce8c3..ff1d7c28a7 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/package-info.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/package-info.java @@ -1,5 +1,5 @@ /** * Provides classes for stream listeners. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.rabbit.stream.listener; diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamMessageReceiverContext.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamMessageReceiverContext.java index c2a2656860..020fbba8df 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamMessageReceiverContext.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamMessageReceiverContext.java @@ -38,10 +38,9 @@ public class RabbitStreamMessageReceiverContext extends ReceiverContext private final String listenerId; - private final Message message; - private final String stream; + @SuppressWarnings("this-escape") public RabbitStreamMessageReceiverContext(Message message, String listenerId, String stream) { super((carrier, key) -> { Map props = carrier.getApplicationProperties(); @@ -57,7 +56,6 @@ else if (value instanceof byte[] bytes) { return null; }); setCarrier(message); - this.message = message; this.listenerId = listenerId; this.stream = stream; setRemoteServiceName("RabbitMQ Stream"); diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamMessageSenderContext.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamMessageSenderContext.java index d8afcb264f..b2ee9819b6 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamMessageSenderContext.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/RabbitStreamMessageSenderContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2023-2025 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. @@ -34,6 +34,7 @@ public class RabbitStreamMessageSenderContext extends SenderContext { private final String destination; + @SuppressWarnings("this-escape") public RabbitStreamMessageSenderContext(Message message, String beanName, String destination) { super((carrier, key, value) -> { Map props = message.getApplicationProperties(); diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/package-info.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/package-info.java index 14fb3141b3..28a009bc25 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/package-info.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/micrometer/package-info.java @@ -1,6 +1,5 @@ /** * Provides classes for Micrometer support. */ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.rabbit.stream.micrometer; diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations.java index 80977ac8b0..f4ffe4f6c6 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamOperations.java @@ -19,12 +19,12 @@ import java.util.concurrent.CompletableFuture; import com.rabbitmq.stream.MessageBuilder; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpException; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessagePostProcessor; import org.springframework.amqp.support.converter.MessageConverter; -import org.springframework.lang.Nullable; import org.springframework.rabbit.stream.support.converter.StreamMessageConverter; /** diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java index b2bde13775..f816da8ac5 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/RabbitStreamTemplate.java @@ -29,6 +29,7 @@ import com.rabbitmq.stream.ProducerBuilder; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessagePostProcessor; @@ -40,7 +41,6 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.log.LogAccessor; -import org.springframework.lang.Nullable; import org.springframework.rabbit.stream.micrometer.RabbitStreamMessageSenderContext; import org.springframework.rabbit.stream.micrometer.RabbitStreamTemplateObservation; import org.springframework.rabbit.stream.micrometer.RabbitStreamTemplateObservation.DefaultRabbitStreamTemplateObservationConvention; @@ -65,13 +65,14 @@ public class RabbitStreamTemplate implements RabbitStreamOperations, Application private final Lock lock = new ReentrantLock(); + @SuppressWarnings("NullAway.Init") private ApplicationContext applicationContext; private final Environment environment; private final String streamName; - private Function superStreamRouting; + private @Nullable Function superStreamRouting; private MessageConverter messageConverter = new SimpleMessageConverter(); @@ -79,18 +80,18 @@ public class RabbitStreamTemplate implements RabbitStreamOperations, Application private boolean streamConverterSet; + @SuppressWarnings("NullAway.Init") private String beanName; private ProducerCustomizer producerCustomizer = (name, builder) -> { }; private boolean observationEnabled; - @Nullable - private RabbitStreamTemplateObservationConvention observationConvention; + private @Nullable RabbitStreamTemplateObservationConvention observationConvention; - private ObservationRegistry observationRegistry; + private @Nullable ObservationRegistry observationRegistry; - private volatile Producer producer; + private volatile @Nullable Producer producer; private volatile boolean observationRegistryObtained; @@ -108,10 +109,12 @@ public RabbitStreamTemplate(Environment environment, String streamName) { private Producer createOrGetProducer() { - if (this.producer == null) { + Producer producerToUse = this.producer; + if (producerToUse == null) { this.lock.lock(); try { - if (this.producer == null) { + producerToUse = this.producer; + if (producerToUse == null) { ProducerBuilder builder = this.environment.producerBuilder(); if (this.superStreamRouting == null) { builder.stream(this.streamName); @@ -121,10 +124,11 @@ private Producer createOrGetProducer() { .routing(this.superStreamRouting); } this.producerCustomizer.accept(this.beanName, builder); - this.producer = builder.build(); + producerToUse = builder.build(); + this.producer = producerToUse; if (!this.streamConverterSet) { - ((DefaultStreamMessageConverter) this.streamConverter).setBuilderSupplier( - () -> this.producer.messageBuilder()); + ((DefaultStreamMessageConverter) this.streamConverter) + .setBuilderSupplier(producerToUse::messageBuilder); } } } @@ -132,7 +136,7 @@ private Producer createOrGetProducer() { this.lock.unlock(); } } - return this.producer; + return producerToUse; } @Override @@ -245,15 +249,8 @@ public CompletableFuture convertAndSend(Object message) { @Override public CompletableFuture convertAndSend(Object message, @Nullable MessagePostProcessor mpp) { Message message2 = this.messageConverter.toMessage(message, new StreamMessageProperties()); - Assert.notNull(message2, "The message converter returned null"); if (mpp != null) { message2 = mpp.postProcessMessage(message2); - if (message2 == null) { - this.logger.debug("Message Post Processor returned null, message not sent"); - CompletableFuture future = new CompletableFuture<>(); - future.complete(false); - return future; - } } return send(message2); } @@ -266,6 +263,7 @@ public CompletableFuture send(com.rabbitmq.stream.Message message) { return future; } + @SuppressWarnings({ "NullAway", "try" }) // Dataflow analysis limitation private void observeSend(com.rabbitmq.stream.Message message, CompletableFuture future) { Observation observation = RabbitStreamTemplateObservation.STREAM_TEMPLATE_OBSERVATION.observation( this.observationConvention, DefaultRabbitStreamTemplateObservationConvention.INSTANCE, @@ -282,20 +280,18 @@ private void observeSend(com.rabbitmq.stream.Message message, CompletableFuture< } } - @Nullable - private ObservationRegistry obtainObservationRegistry() { + private @Nullable ObservationRegistry obtainObservationRegistry() { if (!this.observationRegistryObtained && this.observationEnabled) { - if (this.applicationContext != null) { - ObjectProvider registry = - this.applicationContext.getBeanProvider(ObservationRegistry.class); - this.observationRegistry = registry.getIfUnique(); - } + ObjectProvider registry = + this.applicationContext.getBeanProvider(ObservationRegistry.class); + this.observationRegistry = registry.getIfUnique(); this.observationRegistryObtained = true; } return this.observationRegistry; } @Override + @SuppressWarnings("try") public MessageBuilder messageBuilder() { return createOrGetProducer().messageBuilder(); } @@ -334,8 +330,9 @@ public void close() { if (this.producer != null) { this.lock.lock(); try { - if (this.producer != null) { - this.producer.close(); + Producer producerToCheck = this.producer; + if (producerToCheck != null) { + producerToCheck.close(); this.producer = null; } } diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/StreamSendException.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/StreamSendException.java index de11d4abf3..49f03ad2b8 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/StreamSendException.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/StreamSendException.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2025 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. @@ -16,6 +16,8 @@ package org.springframework.rabbit.stream.producer; +import java.io.Serial; + import org.springframework.amqp.AmqpException; /** @@ -27,6 +29,7 @@ */ public class StreamSendException extends AmqpException { + @Serial private static final long serialVersionUID = 1L; private final int confirmationCode; diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/package-info.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/package-info.java index ebf6d3c825..553b453556 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/package-info.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/producer/package-info.java @@ -1,5 +1,5 @@ /** * Provides classes for stream producers. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.rabbit.stream.producer; diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/package-info.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/package-info.java index cabb5d622f..98aa4fd99d 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/package-info.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/retry/package-info.java @@ -1,4 +1,5 @@ /** * Provides classes supporting retries. */ +@org.jspecify.annotations.NullMarked package org.springframework.rabbit.stream.retry; diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/StreamMessageProperties.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/StreamMessageProperties.java index 36eb6d0c57..1e8c5b5357 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/StreamMessageProperties.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/StreamMessageProperties.java @@ -16,12 +16,13 @@ package org.springframework.rabbit.stream.support; +import java.io.Serial; import java.util.Objects; import com.rabbitmq.stream.MessageHandler.Context; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.MessageProperties; -import org.springframework.lang.Nullable; /** * {@link MessageProperties} extension for stream messages. @@ -32,24 +33,25 @@ */ public class StreamMessageProperties extends MessageProperties { + @Serial private static final long serialVersionUID = 1L; - private transient Context context; + private transient @Nullable Context context; - private String to; + private @Nullable String to; - private String subject; + private @Nullable String subject; private long creationTime; - private String groupId; + private @Nullable String groupId; private long groupSequence; - private String replyToGroupId; + private @Nullable String replyToGroupId; /** - * Create a new instance. + * Create a new instance. */ public StreamMessageProperties() { } @@ -66,8 +68,7 @@ public StreamMessageProperties(@Nullable Context context) { * Return the stream {@link Context} for the message. * @return the context. */ - @Nullable - public Context getContext() { + public @Nullable Context getContext() { return this.context; } @@ -75,7 +76,7 @@ public Context getContext() { * See {@link com.rabbitmq.stream.Properties#getTo()}. * @return the to address. */ - public String getTo() { + public @Nullable String getTo() { return this.to; } @@ -91,7 +92,7 @@ public void setTo(String address) { * See {@link com.rabbitmq.stream.Properties#getSubject()}. * @return the subject. */ - public String getSubject() { + public @Nullable String getSubject() { return this.subject; } @@ -124,7 +125,7 @@ public void setCreationTime(long creationTime) { * See {@link com.rabbitmq.stream.Properties#getGroupId()}. * @return the group id. */ - public String getGroupId() { + public @Nullable String getGroupId() { return this.groupId; } @@ -157,7 +158,7 @@ public void setGroupSequence(long groupSequence) { * See {@link com.rabbitmq.stream.Properties#getReplyToGroupId()}. * @return the reply to group id. */ - public String getReplyToGroupId() { + public @Nullable String getReplyToGroupId() { return this.replyToGroupId; } diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverter.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverter.java index b50c797a17..00a72cdb98 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverter.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/DefaultStreamMessageConverter.java @@ -33,7 +33,6 @@ import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.support.converter.MessageConversionException; import org.springframework.amqp.utils.JavaUtils; -import org.springframework.lang.Nullable; import org.springframework.rabbit.stream.support.StreamMessageProperties; import org.springframework.util.Assert; @@ -55,15 +54,15 @@ public class DefaultStreamMessageConverter implements StreamMessageConverter { * Construct an instance using a {@link WrapperMessageBuilder}. */ public DefaultStreamMessageConverter() { - this.builderSupplier = () -> new WrapperMessageBuilder(); + this.builderSupplier = WrapperMessageBuilder::new; } /** * Construct an instance using the provided codec. * @param codec the codec. */ - public DefaultStreamMessageConverter(@Nullable Codec codec) { - this.builderSupplier = () -> codec.messageBuilder(); + public DefaultStreamMessageConverter(Codec codec) { + this.builderSupplier = codec::messageBuilder; } /** diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/package-info.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/package-info.java index 5ed17935fc..aa6dca80bf 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/package-info.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/converter/package-info.java @@ -1,5 +1,5 @@ /** * Provides classes for message conversion. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.rabbit.stream.support.converter; diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/package-info.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/package-info.java index c0eb78691c..a8720b68b4 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/package-info.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/support/package-info.java @@ -1,5 +1,5 @@ /** * Provides support classes. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.rabbit.stream.support; diff --git a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java index be41f1593e..102855c959 100644 --- a/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java +++ b/spring-rabbit-stream/src/test/java/org/springframework/rabbit/stream/listener/RabbitListenerTests.java @@ -95,8 +95,6 @@ void simple(@Autowired RabbitStreamTemplate template, @Autowired MeterRegistry m assertThat(future.get(10, TimeUnit.SECONDS)).isTrue(); future = template.send(template.messageBuilder().addData("qux".getBytes()).build()); assertThat(future.get(10, TimeUnit.SECONDS)).isTrue(); - future = template.convertAndSend("bar", msg -> null); - assertThat(future.get(10, TimeUnit.SECONDS)).isFalse(); assertThat(this.config.latch1.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(this.config.received).containsExactly("foo", "foo", "bar", "baz", "qux"); assertThat(this.config.id).isEqualTo("testNative"); @@ -154,9 +152,8 @@ private Map queueInfo(String queueName) throws URISyntaxExceptio } private URI queueUri(String queue) throws URISyntaxException { - URI uri = new URI("http://localhost:" + managementPort() + "/api") + return new URI("http://localhost:" + managementPort() + "/api") .resolve("/api/queues/" + UriUtils.encodePathSegment("/", StandardCharsets.UTF_8) + "/" + queue); - return uri; } private WebClient createClient(String adminUser, String adminPassword) { @@ -302,9 +299,7 @@ void listenObs(String in) { @Bean public StreamRetryOperationsInterceptorFactoryBean sfb() { StreamRetryOperationsInterceptorFactoryBean rfb = new StreamRetryOperationsInterceptorFactoryBean(); - rfb.setStreamMessageRecoverer((msg, context, throwable) -> { - this.latch4.countDown(); - }); + rfb.setStreamMessageRecoverer((msg, context, throwable) -> this.latch4.countDown()); return rfb; } diff --git a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/RabbitListenerTestHarness.java b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/RabbitListenerTestHarness.java index 05adc02496..beb74a6088 100644 --- a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/RabbitListenerTestHarness.java +++ b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/RabbitListenerTestHarness.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 the original author or authors. + * Copyright 2016-2025 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. @@ -27,6 +27,7 @@ import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.mockito.AdditionalAnswers; import org.mockito.Mockito; @@ -73,13 +74,15 @@ public class RabbitListenerTestHarness extends RabbitListenerAnnotationBeanPostP private final AnnotationAttributes attributes; public RabbitListenerTestHarness(AnnotationMetadata importMetadata) { - Map map = importMetadata.getAnnotationAttributes(RabbitListenerTest.class.getName()); - this.attributes = AnnotationAttributes.fromMap(map); - Assert.notNull(this.attributes, + Map map = importMetadata.getAnnotationAttributes(RabbitListenerTest.class.getName()); + AnnotationAttributes annotationAttributes = AnnotationAttributes.fromMap(map); + Assert.notNull(annotationAttributes, () -> "@RabbitListenerTest is not present on importing class " + importMetadata.getClassName()); + this.attributes = annotationAttributes; } @Override + @SuppressWarnings("NullAway") // Dataflow analysis limitation protected Collection processListener(MethodRabbitListenerEndpoint endpoint, RabbitListener rabbitListener, Object bean, Object target, String beanName) { @@ -110,7 +113,7 @@ protected Collection processListener(MethodRabbitListenerEndpoint en else { logger.info("The test harness can only proxy @RabbitListeners with an 'id' attribute"); } - return super.processListener(endpoint, rabbitListener, proxy, target, beanName); // NOSONAR proxy is not null + return super.processListener(endpoint, rabbitListener, proxy, target, beanName); } /** @@ -138,7 +141,9 @@ public LambdaAnswer getLambdaAnswerFor(String id, boolean callRealMethod, return new LambdaAnswer<>(callRealMethod, callback, this.delegates.get(id)); } - public InvocationData getNextInvocationDataFor(String id, long wait, TimeUnit unit) throws InterruptedException { + public @Nullable InvocationData getNextInvocationDataFor(String id, long wait, TimeUnit unit) + throws InterruptedException { + CaptureAdvice advice = this.listenerCapture.get(id); if (advice != null) { return advice.invocationData.poll(wait, unit); @@ -147,7 +152,7 @@ public InvocationData getNextInvocationDataFor(String id, long wait, TimeUnit un } @SuppressWarnings("unchecked") - public T getSpy(String id) { + public @Nullable T getSpy(String id) { return (T) this.listeners.get(id); } @@ -159,7 +164,7 @@ public T getSpy(String id) { * @since 2.1.16 */ @SuppressWarnings("unchecked") - public T getDelegate(String id) { + public @Nullable T getDelegate(String id) { return (T) this.delegates.get(id); } @@ -171,7 +176,7 @@ private static final class CaptureAdvice implements MethodInterceptor { } @Override - public Object invoke(MethodInvocation invocation) throws Throwable { + public @Nullable Object invoke(MethodInvocation invocation) throws Throwable { MergedAnnotations annotations = MergedAnnotations.from(invocation.getMethod()); boolean isListenerMethod = annotations.isPresent(RabbitListener.class) || annotations.isPresent(RabbitHandler.class); @@ -196,11 +201,11 @@ public static class InvocationData { private final MethodInvocation invocation; - private final Object result; + private final @Nullable Object result; - private final Throwable throwable; + private final @Nullable Throwable throwable; - public InvocationData(MethodInvocation invocation, Object result) { + public InvocationData(MethodInvocation invocation, @Nullable Object result) { this.invocation = invocation; this.result = result; this.throwable = null; @@ -212,15 +217,15 @@ public InvocationData(MethodInvocation invocation, Throwable throwable) { this.throwable = throwable; } - public Object[] getArguments() { + public @Nullable Object[] getArguments() { return this.invocation.getArguments(); } - public Object getResult() { + public @Nullable Object getResult() { return this.result; } - public Throwable getThrowable() { + public @Nullable Throwable getThrowable() { return this.throwable; } diff --git a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/TestRabbitTemplate.java b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/TestRabbitTemplate.java index d9ac0ee452..f3e2af5891 100644 --- a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/TestRabbitTemplate.java +++ b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/TestRabbitTemplate.java @@ -30,6 +30,7 @@ import com.rabbitmq.client.AMQP.BasicProperties; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Envelope; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageBuilder; @@ -48,6 +49,7 @@ import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.util.Assert; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -74,11 +76,13 @@ public class TestRabbitTemplate extends RabbitTemplate private final Map listeners = new HashMap<>(); + @SuppressWarnings("NullAway.Init") private ApplicationContext applicationContext; @Autowired private RabbitListenerEndpointRegistry registry; + @SuppressWarnings("this-escape") public TestRabbitTemplate(ConnectionFactory connectionFactory) { super(connectionFactory); setReplyAddress(REPLY_QUEUE); @@ -108,7 +112,9 @@ public void onApplicationEvent(ContextRefreshedEvent event) { } private void setupListener(AbstractMessageListenerContainer container, String queue) { - this.listeners.computeIfAbsent(queue, v -> new Listeners()).listeners.add(container.getMessageListener()); + MessageListener messageListener = container.getMessageListener(); + Assert.notNull(messageListener, "'container.getMessageListener()' must not be null"); + this.listeners.computeIfAbsent(queue, v -> new Listeners()).listeners.add(messageListener); } @Override @@ -133,8 +139,8 @@ protected void sendToRabbit(Channel channel, String exchange, String routingKey, } @Override - protected Message doSendAndReceiveWithFixed(String exchange, String routingKey, Message message, - CorrelationData correlationData) { + protected @Nullable Message doSendAndReceiveWithFixed(@Nullable String exchange, @Nullable String routingKey, + Message message, @Nullable CorrelationData correlationData) { Listeners listenersForRoute = this.listeners.get(routingKey); if (listenersForRoute == null) { @@ -163,7 +169,7 @@ protected Message doSendAndReceiveWithFixed(String exchange, String routingKey, } } else { - throw new IllegalStateException("sendAndReceive not supported for " + listener.getClass().getName()); + throw new IllegalStateException("sendAndReceive not supported for " + listener); } return reply.get(); } @@ -192,7 +198,7 @@ private static class Listeners { private final List listeners = new ArrayList<>(); - private volatile Iterator iterator; + private volatile @Nullable Iterator iterator; Listeners() { } @@ -200,10 +206,12 @@ private static class Listeners { private Object next() { this.lock.lock(); try { - if (this.iterator == null || !this.iterator.hasNext()) { - this.iterator = this.listeners.iterator(); + Iterator iteratorToUse = this.iterator; + if (iteratorToUse == null || !iteratorToUse.hasNext()) { + iteratorToUse = this.listeners.iterator(); } - return this.iterator.next(); + this.iterator = iteratorToUse; + return iteratorToUse.next(); } finally { this.lock.unlock(); diff --git a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/context/SpringRabbitContextCustomizerFactory.java b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/context/SpringRabbitContextCustomizerFactory.java index 8d7cc16d72..1367702c32 100644 --- a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/context/SpringRabbitContextCustomizerFactory.java +++ b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/context/SpringRabbitContextCustomizerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2025 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. @@ -18,6 +18,8 @@ import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextCustomizer; @@ -36,7 +38,7 @@ class SpringRabbitContextCustomizerFactory implements ContextCustomizerFactory { @Override - public ContextCustomizer createContextCustomizer(Class testClass, + public @Nullable ContextCustomizer createContextCustomizer(Class testClass, List configAttributes) { SpringRabbitTest test = AnnotatedElementUtils.findMergedAnnotation(testClass, SpringRabbitTest.class); diff --git a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/context/package-info.java b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/context/package-info.java index 3f1e810f32..47f51eeb29 100644 --- a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/context/package-info.java +++ b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/context/package-info.java @@ -1,4 +1,5 @@ /** * Provides classes relating to the test application context. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.test.context; diff --git a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LambdaAnswer.java b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LambdaAnswer.java index c67fe71c96..82d16d499b 100644 --- a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LambdaAnswer.java +++ b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LambdaAnswer.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 the original author or authors. + * Copyright 2016-2025 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. @@ -21,11 +21,10 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; import org.mockito.internal.stubbing.defaultanswers.ForwardsInvocations; import org.mockito.invocation.InvocationOnMock; -import org.springframework.lang.Nullable; - /** * An {@link org.mockito.stubbing.Answer} to optionally call the real method and allow * returning a custom result. Captures any exceptions thrown. @@ -96,7 +95,7 @@ public Collection getExceptions() { @FunctionalInterface public interface ValueToReturn { - T apply(InvocationOnMock invocation, T result); + T apply(InvocationOnMock invocation, @Nullable T result); } diff --git a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LatchCountDownAndCallRealMethodAnswer.java b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LatchCountDownAndCallRealMethodAnswer.java index 08b65fafc6..bd3dd8510b 100644 --- a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LatchCountDownAndCallRealMethodAnswer.java +++ b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/LatchCountDownAndCallRealMethodAnswer.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 the original author or authors. + * Copyright 2016-2025 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. @@ -16,6 +16,7 @@ package org.springframework.amqp.rabbit.test.mockito; +import java.io.Serial; import java.util.Collection; import java.util.LinkedHashSet; import java.util.Set; @@ -23,11 +24,10 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import org.jspecify.annotations.Nullable; import org.mockito.internal.stubbing.defaultanswers.ForwardsInvocations; import org.mockito.invocation.InvocationOnMock; -import org.springframework.lang.Nullable; - /** * An {@link org.mockito.stubbing.Answer} for void returning methods that calls the real * method and counts down a latch. Captures any exceptions thrown. @@ -40,11 +40,12 @@ */ public class LatchCountDownAndCallRealMethodAnswer extends ForwardsInvocations { + @Serial private static final long serialVersionUID = 1L; private final transient CountDownLatch latch; - private final Set exceptions = ConcurrentHashMap.newKeySet(); + private final transient Set exceptions = ConcurrentHashMap.newKeySet(); private final boolean hasDelegate; @@ -62,7 +63,7 @@ public LatchCountDownAndCallRealMethodAnswer(int count, @Nullable Object delegat } @Override - public Object answer(InvocationOnMock invocation) throws Throwable { + public @Nullable Object answer(InvocationOnMock invocation) throws Throwable { try { if (this.hasDelegate) { return super.answer(invocation); diff --git a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/package-info.java b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/package-info.java index 9ac27fe258..f4cf10b7a1 100644 --- a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/package-info.java +++ b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/mockito/package-info.java @@ -1,4 +1,5 @@ /** * Mockito extensions for testing Spring AMQP applications. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.test.mockito; diff --git a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/package-info.java b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/package-info.java index d592c60ae6..f1b17543c1 100644 --- a/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/package-info.java +++ b/spring-rabbit-test/src/main/java/org/springframework/amqp/rabbit/test/package-info.java @@ -1,4 +1,5 @@ /** * Classes for testing Spring AMQP applications. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.test; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java index 0389febb5c..0972747d42 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java @@ -17,6 +17,7 @@ package org.springframework.amqp.rabbit; import java.time.Instant; +import java.util.Objects; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; @@ -28,9 +29,9 @@ import com.rabbitmq.client.Channel; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpException; -import org.springframework.amqp.AmqpIllegalStateException; import org.springframework.amqp.core.Address; import org.springframework.amqp.core.AmqpMessageReturnedException; import org.springframework.amqp.core.AsyncAmqpTemplate; @@ -58,8 +59,6 @@ import org.springframework.context.SmartLifecycle; import org.springframework.core.ParameterizedTypeReference; import org.springframework.expression.Expression; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.util.Assert; @@ -104,11 +103,11 @@ public class AsyncRabbitTemplate implements AsyncAmqpTemplate, ChannelAwareMessa private final RabbitTemplate template; - private final AbstractMessageListenerContainer container; + private final @Nullable AbstractMessageListenerContainer container; - private final DirectReplyToMessageListenerContainer directReplyToContainer; + private final @Nullable DirectReplyToMessageListenerContainer directReplyToContainer; - private final String replyAddress; + private final @Nullable String replyAddress; private final ConcurrentMap> pending = new ConcurrentHashMap<>(); @@ -124,8 +123,10 @@ public class AsyncRabbitTemplate implements AsyncAmqpTemplate, ChannelAwareMessa private boolean autoStartup = true; + @SuppressWarnings("NullAway.Init") private String beanName; + @SuppressWarnings("NullAway.Init") private TaskScheduler taskScheduler; private boolean internalTaskScheduler = true; @@ -141,13 +142,14 @@ public class AsyncRabbitTemplate implements AsyncAmqpTemplate, ChannelAwareMessa */ public AsyncRabbitTemplate(ConnectionFactory connectionFactory, String exchange, String routingKey, String replyQueue) { + this(connectionFactory, exchange, routingKey, replyQueue, null); } /** * Construct an instance using the provided arguments. If 'replyAddress' is null, * replies will be routed to the default exchange using the reply queue name as the - * routing key. Otherwise it should have the form exchange/routingKey and must + * routing key. Otherwise, it should have the form exchange/routingKey and must * cause messages to be routed to the reply queue. * @param connectionFactory the connection factory. * @param exchange the default exchange to which requests will be sent. @@ -155,8 +157,10 @@ public AsyncRabbitTemplate(ConnectionFactory connectionFactory, String exchange, * @param replyQueue the name of the reply queue to listen for replies. * @param replyAddress the reply address (exchange/routingKey). */ - public AsyncRabbitTemplate(ConnectionFactory connectionFactory, String exchange, String routingKey, - String replyQueue, String replyAddress) { + @SuppressWarnings("this-escape") + public AsyncRabbitTemplate(ConnectionFactory connectionFactory, @Nullable String exchange, String routingKey, + String replyQueue, @Nullable String replyAddress) { + Assert.notNull(connectionFactory, "'connectionFactory' cannot be null"); Assert.notNull(routingKey, "'routingKey' cannot be null"); Assert.notNull(replyQueue, "'replyQueue' cannot be null"); @@ -172,12 +176,7 @@ public AsyncRabbitTemplate(ConnectionFactory connectionFactory, String exchange, this.container.setMessageListener(this); this.container.afterPropertiesSet(); this.directReplyToContainer = null; - if (replyAddress == null) { - this.replyAddress = replyQueue; - } - else { - this.replyAddress = replyAddress; - } + this.replyAddress = Objects.requireNonNullElse(replyAddress, replyQueue); } @@ -196,26 +195,23 @@ public AsyncRabbitTemplate(RabbitTemplate template, AbstractMessageListenerConta * Construct an instance using the provided arguments. The first queue the container * is configured to listen to will be used as the reply queue. If 'replyAddress' is * null, replies will be routed using the default exchange with that queue name as the - * routing key. Otherwise it should have the form exchange/routingKey and must + * routing key. Otherwise, it should have the form exchange/routingKey and must * cause messages to be routed to the reply queue. * @param template a {@link RabbitTemplate}. * @param container a {@link AbstractMessageListenerContainer}. * @param replyAddress the reply address. */ + @SuppressWarnings("this-escape") public AsyncRabbitTemplate(RabbitTemplate template, AbstractMessageListenerContainer container, - String replyAddress) { + @Nullable String replyAddress) { + Assert.notNull(template, "'template' cannot be null"); Assert.notNull(container, "'container' cannot be null"); this.template = template; this.container = container; this.container.setMessageListener(this); this.directReplyToContainer = null; - if (replyAddress == null) { - this.replyAddress = container.getQueueNames()[0]; - } - else { - this.replyAddress = replyAddress; - } + this.replyAddress = Objects.requireNonNullElseGet(replyAddress, () -> container.getQueueNames()[0]); } /** @@ -226,7 +222,7 @@ public AsyncRabbitTemplate(RabbitTemplate template, AbstractMessageListenerConta * @param routingKey the default routing key. * @since 2.0 */ - public AsyncRabbitTemplate(ConnectionFactory connectionFactory, String exchange, String routingKey) { + public AsyncRabbitTemplate(ConnectionFactory connectionFactory, @Nullable String exchange, String routingKey) { this(new RabbitTemplate(connectionFactory)); Assert.notNull(routingKey, "'routingKey' cannot be null"); this.template.setExchange(exchange == null ? "" : exchange); @@ -239,6 +235,7 @@ public AsyncRabbitTemplate(ConnectionFactory connectionFactory, String exchange, * @param template a {@link RabbitTemplate} * @since 2.0 */ + @SuppressWarnings("this-escape") public AsyncRabbitTemplate(RabbitTemplate template) { Assert.notNull(template, "'template' cannot be null"); this.template = template; @@ -398,6 +395,7 @@ public RabbitMessageFuture sendAndReceive(String exchange, String routingKey, Me this.template.send(exchange, routingKey, message, correlationData); } else { + Assert.notNull(this.directReplyToContainer, "'directReplyToContainer' cannot be null"); ChannelHolder channelHolder = this.directReplyToContainer.getChannelHolder(); future.setChannelHolder(channelHolder); sendDirect(channelHolder.getChannel(), exchange, routingKey, message, correlationData); @@ -431,18 +429,21 @@ public RabbitConverterFuture convertSendAndReceive(Object object, @Override public RabbitConverterFuture convertSendAndReceive(String routingKey, Object object, MessagePostProcessor messagePostProcessor) { + return convertSendAndReceive(this.template.getExchange(), routingKey, object, messagePostProcessor); } @Override public RabbitConverterFuture convertSendAndReceive(String exchange, String routingKey, Object object, - MessagePostProcessor messagePostProcessor) { + @Nullable MessagePostProcessor messagePostProcessor) { + return convertSendAndReceive(exchange, routingKey, object, messagePostProcessor, null); } @Override public RabbitConverterFuture convertSendAndReceiveAsType(Object object, ParameterizedTypeReference responseType) { + return convertSendAndReceiveAsType(this.template.getExchange(), this.template.getRoutingKey(), object, null, responseType); } @@ -450,39 +451,44 @@ public RabbitConverterFuture convertSendAndReceiveAsType(Object object, @Override public RabbitConverterFuture convertSendAndReceiveAsType(String routingKey, Object object, ParameterizedTypeReference responseType) { + return convertSendAndReceiveAsType(this.template.getExchange(), routingKey, object, null, responseType); } @Override public RabbitConverterFuture convertSendAndReceiveAsType(String exchange, String routingKey, Object object, ParameterizedTypeReference responseType) { + return convertSendAndReceiveAsType(exchange, routingKey, object, null, responseType); } @Override public RabbitConverterFuture convertSendAndReceiveAsType(Object object, MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) { + return convertSendAndReceiveAsType(this.template.getExchange(), this.template.getRoutingKey(), object, messagePostProcessor, responseType); } @Override public RabbitConverterFuture convertSendAndReceiveAsType(String routingKey, Object object, - MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) { + @Nullable MessagePostProcessor messagePostProcessor, @Nullable ParameterizedTypeReference responseType) { + return convertSendAndReceiveAsType(this.template.getExchange(), routingKey, object, messagePostProcessor, responseType); } @Override public RabbitConverterFuture convertSendAndReceiveAsType(String exchange, String routingKey, Object object, - MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) { + @Nullable MessagePostProcessor messagePostProcessor, @Nullable ParameterizedTypeReference responseType) { + Assert.state(this.template.getMessageConverter() instanceof SmartMessageConverter, "template's message converter must be a SmartMessageConverter"); return convertSendAndReceive(exchange, routingKey, object, messagePostProcessor, responseType); } private RabbitConverterFuture convertSendAndReceive(String exchange, String routingKey, Object object, - MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) { + @Nullable MessagePostProcessor messagePostProcessor, @Nullable ParameterizedTypeReference responseType) { AsyncCorrelationData correlationData = new AsyncCorrelationData<>(messagePostProcessor, responseType, this.enableConfirms); @@ -491,13 +497,10 @@ private RabbitConverterFuture convertSendAndReceive(String exchange, Stri } else { MessageConverter converter = this.template.getMessageConverter(); - if (converter == null) { - throw new AmqpIllegalStateException( - "No 'messageConverter' specified. Check configuration of RabbitTemplate."); - } Message message = converter.toMessage(object, new MessageProperties()); this.messagePostProcessor.postProcessMessage(message, correlationData, this.template.nullSafeExchange(exchange), this.template.nullSafeRoutingKey(routingKey)); + @SuppressWarnings("NullAway") // Dataflow analysis limitation ChannelHolder channelHolder = this.directReplyToContainer.getChannelHolder(); correlationData.future.setChannelHolder(channelHolder); sendDirect(channelHolder.getChannel(), exchange, routingKey, message, correlationData); @@ -508,7 +511,8 @@ private RabbitConverterFuture convertSendAndReceive(String exchange, Stri } private void sendDirect(Channel channel, String exchange, String routingKey, Message message, - CorrelationData correlationData) { + @Nullable CorrelationData correlationData) { + message.getMessageProperties().setReplyTo(Address.AMQ_RABBITMQ_REPLY_TO); try { if (channel instanceof PublisherCallbackChannel) { @@ -529,7 +533,7 @@ public void start() { if (!this.running) { if (this.internalTaskScheduler) { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); - scheduler.setThreadNamePrefix(getBeanName() == null ? "asyncTemplate-" : (getBeanName() + "-")); + scheduler.setThreadNamePrefix(getBeanName() + "-"); scheduler.afterPropertiesSet(); this.taskScheduler = scheduler; } @@ -549,6 +553,7 @@ public void start() { } @Override + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void stop() { this.lock.lock(); try { @@ -592,39 +597,37 @@ public boolean isAutoStartup() { @SuppressWarnings("unchecked") @Override - public void onMessage(Message message, Channel channel) { + public void onMessage(Message message, @Nullable Channel channel) { MessageProperties messageProperties = message.getMessageProperties(); - if (messageProperties != null) { - String correlationId = messageProperties.getCorrelationId(); - if (StringUtils.hasText(correlationId)) { - if (this.logger.isDebugEnabled()) { - this.logger.debug("onMessage: " + message); - } - RabbitFuture future = this.pending.remove(correlationId); - if (future != null) { - if (future instanceof RabbitConverterFuture) { - MessageConverter messageConverter = this.template.getMessageConverter(); - RabbitConverterFuture rabbitFuture = (RabbitConverterFuture) future; - try { - Object converted = rabbitFuture.getReturnType() != null + String correlationId = messageProperties.getCorrelationId(); + if (StringUtils.hasText(correlationId)) { + if (this.logger.isDebugEnabled()) { + this.logger.debug("onMessage: " + message); + } + RabbitFuture future = this.pending.remove(correlationId); + if (future != null) { + if (future instanceof RabbitConverterFuture) { + MessageConverter messageConverter = this.template.getMessageConverter(); + RabbitConverterFuture rabbitFuture = (RabbitConverterFuture) future; + try { + Object converted = rabbitFuture.getReturnType() != null && messageConverter instanceof SmartMessageConverter smart ? smart.fromMessage(message, rabbitFuture.getReturnType()) : messageConverter.fromMessage(message); - rabbitFuture.complete(converted); - } - catch (MessageConversionException e) { - rabbitFuture.completeExceptionally(e); - } + rabbitFuture.complete(converted); } - else { - ((RabbitMessageFuture) future).complete(message); + catch (MessageConversionException e) { + rabbitFuture.completeExceptionally(e); } } else { - if (this.logger.isWarnEnabled()) { - this.logger.warn("No pending reply - perhaps timed out: " + message); - } + ((RabbitMessageFuture) future).complete(message); + } + } + else { + if (this.logger.isWarnEnabled()) { + this.logger.warn("No pending reply - perhaps timed out: " + message); } } } @@ -649,24 +652,23 @@ public void returnedMessage(ReturnedMessage returned) { } @Override - public void confirm(@NonNull CorrelationData correlationData, boolean ack, @Nullable String cause) { + public void confirm(@Nullable CorrelationData correlationData, boolean ack, @Nullable String cause) { if (this.logger.isDebugEnabled()) { this.logger.debug("Confirm: " + correlationData + ", ack=" + ack + (cause == null ? "" : (", cause: " + cause))); } + Assert.notNull(correlationData, "'correlationData' must not be null"); String correlationId = correlationData.getId(); - if (correlationId != null) { - RabbitFuture future = this.pending.get(correlationId); - if (future != null) { - future.setNackCause(cause); - future.getConfirm().complete(ack); - } - else { - if (this.logger.isDebugEnabled()) { - this.logger.debug("Confirm: " + correlationData + ", ack=" + ack - + (cause == null ? "" : (", cause: " + cause)) - + " no pending future - either canceled or the reply is already received"); - } + RabbitFuture future = this.pending.get(correlationId); + if (future != null) { + future.setNackCause(cause); + future.getConfirm().complete(ack); + } + else { + if (this.logger.isDebugEnabled()) { + this.logger.debug("Confirm: " + correlationData + ", ack=" + ack + + (cause == null ? "" : (", cause: " + cause)) + + " no pending future - either canceled or the reply is already received"); } } } @@ -694,8 +696,7 @@ private void canceler(String correlationId, @Nullable ChannelHolder channelHolde } } - @Nullable - private ScheduledFuture timeoutTask(RabbitFuture future) { + private @Nullable ScheduledFuture timeoutTask(RabbitFuture future) { if (this.receiveTimeout > 0) { this.lock.lock(); try { @@ -716,7 +717,7 @@ private ScheduledFuture timeoutTask(RabbitFuture future) { @Override public String toString() { - return this.beanName == null ? super.toString() : (this.getClass().getSimpleName() + ": " + this.beanName); + return this.getClass().getSimpleName() + ": " + this.beanName; } private final class CorrelationMessagePostProcessor implements MessagePostProcessor { @@ -731,9 +732,10 @@ public Message postProcessMessage(Message message) throws AmqpException { @SuppressWarnings("unchecked") @Override - public Message postProcessMessage(Message message, Correlation correlation) throws AmqpException { + public Message postProcessMessage(Message message, @Nullable Correlation correlation) throws AmqpException { Message messageToSend = message; AsyncCorrelationData correlationData = (AsyncCorrelationData) correlation; + Assert.notNull(correlationData, "correlationData cannot be null"); if (correlationData.userPostProcessor != null) { messageToSend = correlationData.userPostProcessor.postProcessMessage(message); } @@ -753,16 +755,17 @@ public Message postProcessMessage(Message message, Correlation correlation) thro private static class AsyncCorrelationData extends CorrelationData { - final MessagePostProcessor userPostProcessor; // NOSONAR + final @Nullable MessagePostProcessor userPostProcessor; // NOSONAR - final ParameterizedTypeReference returnType; // NOSONAR + final @Nullable ParameterizedTypeReference returnType; // NOSONAR final boolean enableConfirms; // NOSONAR + @SuppressWarnings("NullAway.Init") volatile RabbitConverterFuture future; // NOSONAR - AsyncCorrelationData(MessagePostProcessor userPostProcessor, ParameterizedTypeReference returnType, - boolean enableConfirms) { + AsyncCorrelationData(@Nullable MessagePostProcessor userPostProcessor, + @Nullable ParameterizedTypeReference returnType, boolean enableConfirms) { this.userPostProcessor = userPostProcessor; this.returnType = returnType; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitConverterFuture.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitConverterFuture.java index fe799c577a..7e10cf519a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitConverterFuture.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitConverterFuture.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2025 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. @@ -20,6 +20,8 @@ import java.util.function.BiConsumer; import java.util.function.Function; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.listener.DirectReplyToMessageListenerContainer.ChannelHolder; import org.springframework.core.ParameterizedTypeReference; @@ -34,20 +36,20 @@ */ public class RabbitConverterFuture extends RabbitFuture { - private volatile ParameterizedTypeReference returnType; + private volatile @Nullable ParameterizedTypeReference returnType; RabbitConverterFuture(String correlationId, Message requestMessage, - BiConsumer canceler, - Function, ScheduledFuture> timeoutTaskFunction) { + BiConsumer canceler, + Function, @Nullable ScheduledFuture> timeoutTaskFunction) { super(correlationId, requestMessage, canceler, timeoutTaskFunction); } - public ParameterizedTypeReference getReturnType() { + public @Nullable ParameterizedTypeReference getReturnType() { return this.returnType; } - public void setReturnType(ParameterizedTypeReference returnType) { + public void setReturnType(@Nullable ParameterizedTypeReference returnType) { this.returnType = returnType; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitFuture.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitFuture.java index ab77bcf372..2c32b730f9 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitFuture.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitFuture.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 the original author or authors. + * Copyright 2022-2025 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. @@ -21,6 +21,8 @@ import java.util.function.BiConsumer; import java.util.function.Function; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.listener.DirectReplyToMessageListenerContainer.ChannelHolder; @@ -39,20 +41,22 @@ public abstract class RabbitFuture extends CompletableFuture { private final Message requestMessage; - private final BiConsumer canceler; + private final BiConsumer canceler; - private final Function, ScheduledFuture> timeoutTaskFunction; + private final Function, @Nullable ScheduledFuture> timeoutTaskFunction; - private ScheduledFuture timeoutTask; + private @Nullable ScheduledFuture timeoutTask; + @SuppressWarnings("NullAway.Init") private volatile CompletableFuture confirm; - private String nackCause; + private @Nullable String nackCause; - private ChannelHolder channelHolder; + private @Nullable ChannelHolder channelHolder; - protected RabbitFuture(String correlationId, Message requestMessage, BiConsumer canceler, - Function, ScheduledFuture> timeoutTaskFunction) { + protected RabbitFuture(String correlationId, Message requestMessage, + BiConsumer canceler, + Function, @Nullable ScheduledFuture> timeoutTaskFunction) { this.correlationId = correlationId; this.requestMessage = requestMessage; @@ -68,6 +72,7 @@ String getCorrelationId() { return this.correlationId; } + @Nullable ChannelHolder getChannelHolder() { return this.channelHolder; } @@ -131,11 +136,11 @@ void setConfirm(CompletableFuture confirm) { * the cause for the nack, if any. * @return the cause. */ - public String getNackCause() { + public @Nullable String getNackCause() { return this.nackCause; } - void setNackCause(String nackCause) { + void setNackCause(@Nullable String nackCause) { this.nackCause = nackCause; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitMessageFuture.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitMessageFuture.java index 467053a35d..33b8367998 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitMessageFuture.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/RabbitMessageFuture.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2025 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. @@ -20,6 +20,8 @@ import java.util.function.BiConsumer; import java.util.function.Function; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.listener.DirectReplyToMessageListenerContainer.ChannelHolder; @@ -31,8 +33,9 @@ */ public class RabbitMessageFuture extends RabbitFuture { - RabbitMessageFuture(String correlationId, Message requestMessage, BiConsumer canceler, - Function, ScheduledFuture> timeoutTaskFunction) { + RabbitMessageFuture(String correlationId, Message requestMessage, + BiConsumer canceler, + Function, @Nullable ScheduledFuture> timeoutTaskFunction) { super(correlationId, requestMessage, canceler, timeoutTaskFunction); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/TimeoutTask.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/TimeoutTask.java index 8ca3af1667..b1d2e1e87f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/TimeoutTask.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/TimeoutTask.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2025 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. @@ -18,10 +18,11 @@ import java.util.concurrent.ConcurrentMap; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.AmqpReplyTimeoutException; import org.springframework.amqp.rabbit.listener.DirectReplyToMessageListenerContainer; import org.springframework.amqp.rabbit.listener.DirectReplyToMessageListenerContainer.ChannelHolder; -import org.springframework.lang.Nullable; /** * A {@link Runnable} used to time out a {@link RabbitFuture}. @@ -35,7 +36,7 @@ public class TimeoutTask implements Runnable { private final ConcurrentMap> pending; - private final DirectReplyToMessageListenerContainer container; + private final @Nullable DirectReplyToMessageListenerContainer container; TimeoutTask(RabbitFuture future, ConcurrentMap> pending, @Nullable DirectReplyToMessageListenerContainer container) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/MultiRabbitBootstrapConfiguration.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/MultiRabbitBootstrapConfiguration.java index cfc127cb16..cdd1c63b4d 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/MultiRabbitBootstrapConfiguration.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/MultiRabbitBootstrapConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 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. @@ -16,6 +16,8 @@ package org.springframework.amqp.rabbit.annotation; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.rabbit.config.RabbitListenerConfigUtils; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; @@ -23,7 +25,6 @@ import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.env.Environment; import org.springframework.core.type.AnnotationMetadata; -import org.springframework.lang.Nullable; /** * An {@link ImportBeanDefinitionRegistrar} class that registers @@ -31,6 +32,7 @@ * is enabled. * * @author Wander Costa + * @author Artem Bilan * * @since 1.4 * @@ -41,6 +43,7 @@ */ public class MultiRabbitBootstrapConfiguration implements ImportBeanDefinitionRegistrar, EnvironmentAware { + @SuppressWarnings("NullAway.Init") private Environment environment; @Override diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/MultiRabbitListenerAnnotationBeanPostProcessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/MultiRabbitListenerAnnotationBeanPostProcessor.java index 521adc21e5..3a99b98931 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/MultiRabbitListenerAnnotationBeanPostProcessor.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/MultiRabbitListenerAnnotationBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 the original author or authors. + * Copyright 2020-2025 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. @@ -62,7 +62,7 @@ private RabbitListener proxyIfAdminNotPresent(final RabbitListener rabbitListene return rabbitListener; } return (RabbitListener) Proxy.newProxyInstance( - RabbitListener.class.getClassLoader(), new Class[]{RabbitListener.class}, + RabbitListener.class.getClassLoader(), new Class[] {RabbitListener.class}, new RabbitListenerAdminReplacementInvocationHandler(rabbitListener, rabbitAdmin)); } @@ -104,15 +104,8 @@ protected String resolveMultiRabbitAdminName(RabbitListener rabbitListener) { /** * An {@link InvocationHandler} to provide a replacing admin() parameter of the listener. */ - private static final class RabbitListenerAdminReplacementInvocationHandler implements InvocationHandler { - - private final RabbitListener target; - private final String admin; - - private RabbitListenerAdminReplacementInvocationHandler(final RabbitListener target, final String admin) { - this.target = target; - this.admin = admin; - } + private record RabbitListenerAdminReplacementInvocationHandler(RabbitListener target, + String admin) implements InvocationHandler { @Override public Object invoke(final Object proxy, final Method method, final Object[] args) @@ -122,6 +115,7 @@ public Object invoke(final Object proxy, final Method method, final Object[] arg } return method.invoke(this.target, args); } + } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitBootstrapConfiguration.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitBootstrapConfiguration.java index 9789a3b0d9..3bbcef142a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitBootstrapConfiguration.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitBootstrapConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 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. @@ -16,13 +16,14 @@ package org.springframework.amqp.rabbit.annotation; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.rabbit.config.RabbitListenerConfigUtils; import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.type.AnnotationMetadata; -import org.springframework.lang.Nullable; /** * An {@link ImportBeanDefinitionRegistrar} class that registers diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java index e22e1b3860..60f71ecf67 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 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. @@ -36,6 +36,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.AmqpAdmin; @@ -85,7 +86,6 @@ import org.springframework.core.env.Environment; import org.springframework.core.task.TaskExecutor; import org.springframework.format.support.DefaultFormattingConversionService; -import org.springframework.lang.Nullable; import org.springframework.messaging.converter.GenericMessageConverter; import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory; import org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory; @@ -112,7 +112,7 @@ * *

Auto-detect any {@link RabbitListenerConfigurer} instances in the container, * allowing for customization of the registry to be used, the default container - * factory or for fine-grained control over endpoints registration. See + * factory or for fine-grained control over endpoint registrations. See * {@link EnableRabbit} Javadoc for complete usage details. * * @author Stephane Nicoll @@ -149,12 +149,14 @@ public class RabbitListenerAnnotationBeanPostProcessor private final Set emptyStringArguments = new HashSet<>(); - private RabbitListenerEndpointRegistry endpointRegistry; + private @Nullable RabbitListenerEndpointRegistry endpointRegistry; private String defaultContainerFactoryBeanName = DEFAULT_RABBIT_LISTENER_CONTAINER_FACTORY_BEAN_NAME; + @SuppressWarnings("NullAway.Init") private BeanFactory beanFactory; + @SuppressWarnings("NullAway.Init") private ClassLoader beanClassLoader; private final RabbitHandlerMethodFactoryAdapter messageHandlerMethodFactory = @@ -168,6 +170,7 @@ public class RabbitListenerAnnotationBeanPostProcessor private BeanExpressionResolver resolver = new StandardBeanExpressionResolver(); + @SuppressWarnings("NullAway.Init") private BeanExpressionContext expressionContext; private int increment; @@ -224,7 +227,10 @@ public void setMessageHandlerMethodFactory(MessageHandlerMethodFactory messageHa public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; if (beanFactory instanceof ConfigurableListableBeanFactory clbf) { - this.resolver = clbf.getBeanExpressionResolver(); + BeanExpressionResolver beanExpressionResolver = clbf.getBeanExpressionResolver(); + if (beanExpressionResolver != null) { + this.resolver = beanExpressionResolver; + } this.expressionContext = new BeanExpressionContext(clbf, null); } } @@ -269,8 +275,6 @@ public void afterSingletonsInstantiated() { if (this.registrar.getEndpointRegistry() == null) { if (this.endpointRegistry == null) { - Assert.state(this.beanFactory != null, - "BeanFactory must be set to find endpoint registry by bean name"); this.endpointRegistry = this.beanFactory.getBean( RabbitListenerConfigUtils.RABBIT_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME, RabbitListenerEndpointRegistry.class); @@ -278,9 +282,7 @@ public void afterSingletonsInstantiated() { this.registrar.setEndpointRegistry(this.endpointRegistry); } - if (this.defaultContainerFactoryBeanName != null) { - this.registrar.setContainerFactoryBeanName(this.defaultContainerFactoryBeanName); - } + this.registrar.setContainerFactoryBeanName(this.defaultContainerFactoryBeanName); // Set the custom handler method factory once resolved by the configurer MessageHandlerMethodFactory handlerMethodFactory = this.registrar.getMessageHandlerMethodFactory(); @@ -295,12 +297,6 @@ public void afterSingletonsInstantiated() { this.typeCache.clear(); } - - @Override - public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { - return bean; - } - @Override public Object postProcessAfterInitialization(final Object bean, final String beanName) throws BeansException { Class targetClass = AopUtils.getTargetClass(bean); @@ -325,7 +321,7 @@ private TypeMetadata buildMetadata(Class targetClass) { List listenerAnnotations = findListenerAnnotations(method); if (!listenerAnnotations.isEmpty()) { methods.add(new ListenerMethod(method, - listenerAnnotations.toArray(new RabbitListener[listenerAnnotations.size()]))); + listenerAnnotations.toArray(new RabbitListener[0]))); } if (hasClassLevelListeners) { RabbitHandler rabbitHandler = AnnotationUtils.findAnnotation(method, RabbitHandler.class); @@ -339,9 +335,9 @@ private TypeMetadata buildMetadata(Class targetClass) { return TypeMetadata.EMPTY; } return new TypeMetadata( - methods.toArray(new ListenerMethod[methods.size()]), - multiMethods.toArray(new Method[multiMethods.size()]), - classLevelListeners.toArray(new RabbitListener[classLevelListeners.size()])); + methods.toArray(new ListenerMethod[0]), + multiMethods.toArray(new Method[0]), + classLevelListeners.toArray(new RabbitListener[0])); } private List findListenerAnnotations(AnnotatedElement element) { @@ -369,10 +365,11 @@ private void processMultiMethodListeners(RabbitListener[] classLevelListeners, M Method defaultMethod = null; for (Method method : multiMethods) { Method checked = checkProxy(method, bean); - if (AnnotationUtils.findAnnotation(method, RabbitHandler.class).isDefault()) { // NOSONAR never null + RabbitHandler annotation = AnnotationUtils.findAnnotation(method, RabbitHandler.class); + if (annotation != null && annotation.isDefault()) { final Method toAssert = defaultMethod; Assert.state(toAssert == null, () -> "Only one @RabbitHandler can be marked 'isDefault', found: " - + toAssert.toString() + " and " + method.toString()); + + toAssert + " and " + method); defaultMethod = checked; } checkedMethods.add(checked); @@ -406,6 +403,7 @@ private Method checkProxy(Method methodArg, Object bean) { break; } catch (@SuppressWarnings("unused") NoSuchMethodException noMethod) { + // Ignore } } } @@ -521,7 +519,6 @@ private void resolveAdmin(MethodRabbitListenerEndpoint endpoint, RabbitListener else { String rabbitAdmin = resolveExpressionAsString(rabbitListener.admin(), "admin"); if (StringUtils.hasText(rabbitAdmin)) { - Assert.state(this.beanFactory != null, "BeanFactory must be set to resolve RabbitAdmin by bean name"); try { endpoint.setAdmin(this.beanFactory.getBean(rabbitAdmin, RabbitAdmin.class)); } @@ -546,7 +543,6 @@ private RabbitListenerContainerFactory resolveContainerFactory(RabbitListener String containerFactoryBeanName = resolveExpressionAsString(rabbitListener.containerFactory(), "containerFactory"); if (StringUtils.hasText(containerFactoryBeanName)) { - assertBeanFactory(); try { factory = this.beanFactory.getBean(containerFactoryBeanName, RabbitListenerContainerFactory.class); } @@ -569,7 +565,6 @@ private void resolveExecutor(MethodRabbitListenerEndpoint endpoint, RabbitListen else { String execBeanName = resolveExpressionAsString(rabbitListener.executor(), "executor"); if (StringUtils.hasText(execBeanName)) { - assertBeanFactory(); try { endpoint.setTaskExecutor(this.beanFactory.getBean(execBeanName, TaskExecutor.class)); } @@ -591,7 +586,6 @@ private void resolvePostProcessor(MethodRabbitListenerEndpoint endpoint, RabbitL else { String ppBeanName = resolveExpressionAsString(rabbitListener.replyPostProcessor(), "replyPostProcessor"); if (StringUtils.hasText(ppBeanName)) { - assertBeanFactory(); try { endpoint.setReplyPostProcessor(this.beanFactory.getBean(ppBeanName, ReplyPostProcessor.class)); } @@ -613,7 +607,6 @@ private void resolveMessageConverter(MethodRabbitListenerEndpoint endpoint, Rabb else { String mcBeanName = resolveExpressionAsString(rabbitListener.messageConverter(), "messageConverter"); if (StringUtils.hasText(mcBeanName)) { - assertBeanFactory(); try { endpoint.setMessageConverter(this.beanFactory.getBean(mcBeanName, MessageConverter.class)); } @@ -633,10 +626,6 @@ private void resolveReplyContentType(MethodRabbitListenerEndpoint endpoint, Rabb } } - protected void assertBeanFactory() { - Assert.state(this.beanFactory != null, "BeanFactory must be set to obtain container factory by bean name"); - } - protected String noBeanFoundMessage(Object target, String listenerBeanName, String requestedBeanName, Class expectedClass) { return "Could not register rabbit listener endpoint on [" @@ -700,8 +689,8 @@ private void resolveQueues(String queue, List result, List queueB } @SuppressWarnings("unchecked") - private void resolveAsStringOrQueue(Object resolvedValue, List names, @Nullable List queues, - String what) { + private void resolveAsStringOrQueue(@Nullable Object resolvedValue, List names, + @Nullable List queues, String what) { Object resolvedValueToUse = resolvedValue; if (resolvedValue instanceof String[] strings) { @@ -770,6 +759,7 @@ private String declareQueue(org.springframework.amqp.rabbit.annotation.Queue bin return queueName; } + @SuppressWarnings("NullAway") // Dataflow analysis limitation private void declareExchangeAndBinding(QueueBinding binding, String queueName, Collection declarables) { org.springframework.amqp.rabbit.annotation.Exchange bindingExchange = binding.exchange(); String exchangeName = resolveExpressionAsString(bindingExchange.value(), "@Exchange.exchange"); @@ -802,7 +792,7 @@ private void declareExchangeAndBinding(QueueBinding binding, String queueName, C exchangeBuilder.admins((Object[]) bindingExchange.admins()); } - Map arguments = resolveArguments(bindingExchange.arguments()); + Map arguments = resolveArguments(bindingExchange.arguments()); if (!CollectionUtils.isEmpty(arguments)) { exchangeBuilder.withArguments(arguments); @@ -821,6 +811,7 @@ private void declareExchangeAndBinding(QueueBinding binding, String queueName, C private void registerBindings(QueueBinding binding, String queueName, String exchangeName, String exchangeType, Collection declarables) { + final List routingKeys; if (exchangeType.equals(ExchangeTypes.FANOUT) || binding.key().length == 0) { routingKeys = Collections.singletonList(""); @@ -832,7 +823,7 @@ private void registerBindings(QueueBinding binding, String queueName, String exc resolveAsStringOrQueue(resolveExpression(binding.key()[i]), routingKeys, null, "@QueueBinding.key"); } } - final Map bindingArguments = resolveArguments(binding.arguments()); + final Map bindingArguments = resolveArguments(binding.arguments()); final boolean bindingIgnoreExceptions = resolveExpressionAsBoolean(binding.ignoreDeclarationExceptions()); boolean declare = resolveExpressionAsBoolean(binding.declare()); for (String routingKey : routingKeys) { @@ -849,8 +840,8 @@ private void registerBindings(QueueBinding binding, String queueName, String exc } } - private Map resolveArguments(Argument[] arguments) { - Map map = new HashMap<>(); + private @Nullable Map resolveArguments(Argument[] arguments) { + Map map = new HashMap<>(); for (Argument arg : arguments) { String key = resolveExpressionAsString(arg.name(), "@Argument.name"); if (StringUtils.hasText(key)) { @@ -863,8 +854,8 @@ private Map resolveArguments(Argument[] arguments) { typeName = typeClass.getName(); } else { - Assert.isTrue(type instanceof String, () -> "Type must resolve to a Class or String, but resolved to [" - + type.getClass().getName() + "]"); + Assert.isTrue(type instanceof String, () -> "Type must resolve to a Class or String, " + + "but resolved to [" + type + "]"); typeName = (String) type; try { typeClass = ClassUtils.forName(typeName, this.beanClassLoader); @@ -873,18 +864,18 @@ private Map resolveArguments(Argument[] arguments) { throw new IllegalStateException("Could not load class", e); } } - addToMap(map, key, value, typeClass, typeName); + addToMap(map, key, value == null ? "" : value, typeClass, typeName); } else { - if (this.logger.isDebugEnabled()) { - this.logger.debug("@Argument ignored because the name resolved to an empty String"); - } + this.logger.debug("@Argument ignored because the name resolved to an empty String"); } } return map.isEmpty() ? null : map; } - private void addToMap(Map map, String key, Object value, Class typeClass, String typeName) { + private void addToMap(Map map, String key, Object value, Class typeClass, + String typeName) { + if (value.getClass().getName().equals(typeName)) { if (typeClass.equals(String.class) && !StringUtils.hasText((String) value)) { putEmpty(map, key); @@ -909,7 +900,7 @@ private void addToMap(Map map, String key, Object value, Class map, String key) { + private void putEmpty(Map map, String key) { if (this.emptyStringArguments.contains(key)) { map.put(key, ""); } @@ -942,11 +933,11 @@ protected String resolveExpressionAsString(String value, String attribute) { } else { throw new IllegalStateException("The [" + attribute + "] must resolve to a String. " - + "Resolved to [" + resolved.getClass() + "] for [" + value + "]"); + + "Resolved to [" + resolved + "] for [" + value + "]"); } } - private String resolveExpressionAsStringOrInteger(String value, String attribute) { + private @Nullable String resolveExpressionAsStringOrInteger(String value, String attribute) { if (!StringUtils.hasLength(value)) { return null; } @@ -959,11 +950,11 @@ else if (resolved instanceof Integer) { } else { throw new IllegalStateException("The [" + attribute + "] must resolve to a String. " - + "Resolved to [" + resolved.getClass() + "] for [" + value + "]"); + + "Resolved to [" + resolved + "] for [" + value + "]"); } } - protected Object resolveExpression(String value) { + protected @Nullable Object resolveExpression(String value) { String resolvedValue = resolve(value); return this.resolver.evaluate(resolvedValue, this.expressionContext); @@ -975,8 +966,8 @@ protected Object resolveExpression(String value) { * @return the resolved value. * @see ConfigurableBeanFactory#resolveEmbeddedValue */ - private String resolve(String value) { - if (this.beanFactory != null && this.beanFactory instanceof ConfigurableBeanFactory cbf) { + private @Nullable String resolve(String value) { + if (this.beanFactory instanceof ConfigurableBeanFactory cbf) { return cbf.resolveEmbeddedValue(value); } return value; @@ -993,7 +984,7 @@ private class RabbitHandlerMethodFactoryAdapter implements MessageHandlerMethodF private final DefaultFormattingConversionService defaultFormattingConversionService = new DefaultFormattingConversionService(); - private MessageHandlerMethodFactory factory; + private @Nullable MessageHandlerMethodFactory factory; RabbitHandlerMethodFactoryAdapter() { } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/package-info.java index 3cd91d6180..b46a1207f5 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/package-info.java @@ -2,4 +2,5 @@ * Annotations and supporting classes for declarative Rabbit listener * endpoint */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.annotation; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/RabbitRuntimeHints.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/RabbitRuntimeHints.java index 9b4bea3b9b..11f90e8124 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/RabbitRuntimeHints.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/RabbitRuntimeHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2025 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. @@ -16,6 +16,8 @@ package org.springframework.amqp.rabbit.aot; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.rabbit.connection.ChannelProxy; import org.springframework.amqp.rabbit.connection.PublisherCallbackChannel; import org.springframework.aop.SpringProxy; @@ -25,7 +27,6 @@ import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.TypeReference; import org.springframework.core.DecoratingProxy; -import org.springframework.lang.Nullable; /** * {@link RuntimeHintsRegistrar} for spring-rabbit. @@ -37,7 +38,7 @@ public class RabbitRuntimeHints implements RuntimeHintsRegistrar { @Override - public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { ProxyHints proxyHints = hints.proxies(); proxyHints.registerJdkProxy(ChannelProxy.class); proxyHints.registerJdkProxy(ChannelProxy.class, PublisherCallbackChannel.class); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/package-info.java index 7cf34f813e..31fe4aae08 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/aot/package-info.java @@ -1,6 +1,5 @@ /** * Provides classes to support Spring AOT. */ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.aot; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/BatchingStrategy.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/BatchingStrategy.java index 1fec17fab8..3565d5b8d2 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/BatchingStrategy.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/BatchingStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 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. @@ -20,6 +20,8 @@ import java.util.Date; import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; @@ -41,11 +43,13 @@ public interface BatchingStrategy { * @param message The message. * @return The batched message ({@link MessageBatch}), or null if not ready to release. */ - MessageBatch addToBatch(String exchange, String routingKey, Message message); + @Nullable + MessageBatch addToBatch(@Nullable String exchange, @Nullable String routingKey, Message message); /** * @return the date the next scheduled release should run, or null if no data to release. */ + @Nullable Date nextRelease(); /** diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/MessageBatch.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/MessageBatch.java index 05c17c28e8..ac3759cbcf 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/MessageBatch.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/MessageBatch.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 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. @@ -16,47 +16,49 @@ package org.springframework.amqp.rabbit.batch; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Message; /** * An object encapsulating a {@link Message} containing the batch of messages, * the exchange, and routing key. * + * @param exchange the exchange for batch of messages + * @param routingKey the routing key for batch + * @param message the message with a batch + * * @author Gary Russell + * @author Artem Bilan + * * @since 1.4.1 * */ -public class MessageBatch { - - private final String exchange; - - private final String routingKey; - - private final Message message; - - public MessageBatch(String exchange, String routingKey, Message message) { - this.exchange = exchange; - this.routingKey = routingKey; - this.message = message; - } +public record MessageBatch(@Nullable String exchange, @Nullable String routingKey, Message message) { /** * @return the exchange + * @deprecated in favor or {@link #exchange()}. */ - public String getExchange() { + @Deprecated(forRemoval = true, since = "4.0") + public @Nullable String getExchange() { return this.exchange; } /** * @return the routingKey + * @deprecated in favor or {@link #routingKey()}. */ - public String getRoutingKey() { + @Deprecated(forRemoval = true, since = "4.0") + public @Nullable String getRoutingKey() { return this.routingKey; } /** * @return the message + * @deprecated in favor or {@link #message()} ()}. */ + @Deprecated(forRemoval = true, since = "4.0") public Message getMessage() { return this.message; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/SimpleBatchingStrategy.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/SimpleBatchingStrategy.java index bd458351c5..3f1bbb0342 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/SimpleBatchingStrategy.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/SimpleBatchingStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 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. @@ -24,6 +24,8 @@ import java.util.List; import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; @@ -53,9 +55,9 @@ public class SimpleBatchingStrategy implements BatchingStrategy { private final List messages = new ArrayList<>(); - private String exchange; + private @Nullable String exchange; - private String routingKey; + private @Nullable String routingKey; private int currentSize; @@ -72,7 +74,7 @@ public SimpleBatchingStrategy(int batchSize, int bufferLimit, long timeout) { } @Override - public MessageBatch addToBatch(String exch, String routKey, Message message) { + public @Nullable MessageBatch addToBatch(@Nullable String exch, @Nullable String routKey, Message message) { if (this.exchange != null) { Assert.isTrue(this.exchange.equals(exch), "Cannot send to different exchanges in the same batch"); } @@ -103,7 +105,7 @@ public MessageBatch addToBatch(String exch, String routKey, Message message) { } @Override - public Date nextRelease() { + public @Nullable Date nextRelease() { if (this.messages.isEmpty() || this.timeout <= 0) { return null; } @@ -125,7 +127,7 @@ public Collection releaseBatches() { return Collections.singletonList(batch); } - private MessageBatch doReleaseBatch() { + private @Nullable MessageBatch doReleaseBatch() { if (this.messages.isEmpty()) { return null; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/package-info.java index 9901fe182b..eacc718a85 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/batch/package-info.java @@ -1,4 +1,5 @@ /** * Provides classes for message batching. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.batch; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractExchangeParser.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractExchangeParser.java index 434e57d676..4a83e75e99 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractExchangeParser.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractExchangeParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -18,6 +18,7 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.beans.factory.config.BeanDefinition; @@ -39,7 +40,7 @@ */ public abstract class AbstractExchangeParser extends AbstractSingleBeanDefinitionParser { - private static final ThreadLocal CURRENT_ELEMENT = new ThreadLocal<>(); + private static final ThreadLocal<@Nullable Element> CURRENT_ELEMENT = new ThreadLocal<>(); private static final String ARGUMENTS_ELEMENT = "exchange-arguments"; @@ -89,7 +90,7 @@ protected void doParse(Element element, ParserContext parserContext, BeanDefinit NamespaceUtils.setValueIfAttributeDefined(builder, element, DELAYED_ATTRIBUTE); NamespaceUtils.setValueIfAttributeDefined(builder, element, "internal"); - this.parseArguments(element, ARGUMENTS_ELEMENT, parserContext, builder, null); + parseArguments(element, ARGUMENTS_ELEMENT, parserContext, builder, null); NamespaceUtils.parseDeclarationControls(element, builder); CURRENT_ELEMENT.set(element); @@ -97,12 +98,14 @@ protected void doParse(Element element, ParserContext parserContext, BeanDefinit protected void parseBindings(Element element, ParserContext parserContext, BeanDefinitionBuilder builder, String exchangeName) { + Element bindingsElement = DomUtils.getChildElementByTagName(element, BINDINGS_ELE); doParseBindings(element, parserContext, exchangeName, bindingsElement, this); } protected void doParseBindings(Element element, ParserContext parserContext, - String exchangeName, Element bindings, AbstractExchangeParser parser) { + String exchangeName, @Nullable Element bindings, AbstractExchangeParser parser) { + if (bindings != null) { for (Element binding : DomUtils.getChildElementsByTagName(bindings, BINDING_ELE)) { BeanDefinitionBuilder bindingBuilder = parser.parseBinding(exchangeName, binding, @@ -137,7 +140,7 @@ protected void parseDestination(Element binding, ParserContext parserContext, Be } private void parseArguments(Element element, String argumentsElementName, ParserContext parserContext, - BeanDefinitionBuilder builder, String propertyName) { + BeanDefinitionBuilder builder, @Nullable String propertyName) { Element argumentsElement = DomUtils.getChildElementByTagName(element, argumentsElementName); if (argumentsElement != null) { @@ -145,7 +148,7 @@ private void parseArguments(Element element, String argumentsElementName, Parser Map map = parserContext.getDelegate().parseMapElement(argumentsElement, builder.getRawBeanDefinition()); if (StringUtils.hasText(ref)) { - if (map != null && !map.isEmpty()) { + if (!map.isEmpty()) { parserContext.getReaderContext().error("You cannot have both a 'ref' and a nested map", element); } if (propertyName == null) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java index bdbce98737..f51a3d5754 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 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. @@ -16,7 +16,6 @@ package org.springframework.amqp.rabbit.config; - import java.util.Arrays; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; @@ -24,6 +23,7 @@ import org.aopalliance.aop.Advice; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.MessagePostProcessor; @@ -67,57 +67,57 @@ public abstract class AbstractRabbitListenerContainerFactory containerCustomizer; + private @Nullable ContainerCustomizer containerCustomizer; private boolean batchListener; - private BatchingStrategy batchingStrategy; + private @Nullable BatchingStrategy batchingStrategy; - private Boolean deBatchingEnabled; + private @Nullable Boolean deBatchingEnabled; - private MessageAckListener messageAckListener; + private @Nullable MessageAckListener messageAckListener; - private RabbitListenerObservationConvention observationConvention; + private @Nullable RabbitListenerObservationConvention observationConvention; - private Boolean forceStop; + private @Nullable Boolean forceStop; /** * @param connectionFactory The connection factory. @@ -302,9 +302,9 @@ public void setBatchingStrategy(BatchingStrategy batchingStrategy) { } /** - * Determine whether or not the container should de-batch batched + * Determine whether the container should de-batch batched * messages (true) or call the listener with the batch (false). Default: true. - * @param deBatchingEnabled whether or not to disable de-batching of messages. + * @param deBatchingEnabled whether to disable de-batching of messages. * @since 2.2 * @see AbstractMessageListenerContainer#setDeBatchingEnabled(boolean) */ @@ -352,7 +352,7 @@ public void setForceStop(boolean forceStop) { } @Override - public C createListenerContainer(RabbitListenerEndpoint endpoint) { + public C createListenerContainer(@Nullable RabbitListenerEndpoint endpoint) { C instance = createContainerInstance(); JavaUtils javaUtils = @@ -364,42 +364,42 @@ public C createListenerContainer(RabbitListenerEndpoint endpoint) { } Advice[] adviceChain = getAdviceChain(); javaUtils - .acceptIfNotNull(this.acknowledgeMode, instance::setAcknowledgeMode) - .acceptIfNotNull(this.channelTransacted, instance::setChannelTransacted) - .acceptIfNotNull(getApplicationContext(), instance::setApplicationContext) - .acceptIfNotNull(this.taskExecutor, instance::setTaskExecutor) - .acceptIfNotNull(this.transactionManager, instance::setTransactionManager) - .acceptIfNotNull(this.prefetchCount, instance::setPrefetchCount) - .acceptIfNotNull(this.globalQos, instance::setGlobalQos) - .acceptIfNotNull(getDefaultRequeueRejected(), instance::setDefaultRequeueRejected) - .acceptIfNotNull(adviceChain, instance::setAdviceChain) - .acceptIfNotNull(this.recoveryBackOff, instance::setRecoveryBackOff) - .acceptIfNotNull(this.mismatchedQueuesFatal, instance::setMismatchedQueuesFatal) - .acceptIfNotNull(this.missingQueuesFatal, instance::setMissingQueuesFatal) - .acceptIfNotNull(this.consumerTagStrategy, instance::setConsumerTagStrategy) - .acceptIfNotNull(this.idleEventInterval, instance::setIdleEventInterval) - .acceptIfNotNull(this.failedDeclarationRetryInterval, instance::setFailedDeclarationRetryInterval) - .acceptIfNotNull(this.applicationEventPublisher, instance::setApplicationEventPublisher) - .acceptIfNotNull(this.autoStartup, instance::setAutoStartup) - .acceptIfNotNull(this.phase, instance::setPhase) - .acceptIfNotNull(this.afterReceivePostProcessors, instance::setAfterReceivePostProcessors) - .acceptIfNotNull(this.deBatchingEnabled, instance::setDeBatchingEnabled) - .acceptIfNotNull(this.messageAckListener, instance::setMessageAckListener) - .acceptIfNotNull(this.batchingStrategy, instance::setBatchingStrategy) - .acceptIfNotNull(getMicrometerEnabled(), instance::setMicrometerEnabled) - .acceptIfNotNull(getObservationEnabled(), instance::setObservationEnabled) - .acceptIfNotNull(this.observationConvention, instance::setObservationConvention) - .acceptIfNotNull(this.forceStop, instance::setForceStop); + .acceptIfNotNull(this.acknowledgeMode, instance::setAcknowledgeMode) + .acceptIfNotNull(this.channelTransacted, instance::setChannelTransacted) + .acceptIfNotNull(getApplicationContext(), instance::setApplicationContext) + .acceptIfNotNull(this.taskExecutor, instance::setTaskExecutor) + .acceptIfNotNull(this.transactionManager, instance::setTransactionManager) + .acceptIfNotNull(this.prefetchCount, instance::setPrefetchCount) + .acceptIfNotNull(this.globalQos, instance::setGlobalQos) + .acceptIfNotNull(getDefaultRequeueRejected(), instance::setDefaultRequeueRejected) + .acceptIfNotNull(adviceChain, instance::setAdviceChain) + .acceptIfNotNull(this.recoveryBackOff, instance::setRecoveryBackOff) + .acceptIfNotNull(this.mismatchedQueuesFatal, instance::setMismatchedQueuesFatal) + .acceptIfNotNull(this.missingQueuesFatal, instance::setMissingQueuesFatal) + .acceptIfNotNull(this.consumerTagStrategy, instance::setConsumerTagStrategy) + .acceptIfNotNull(this.idleEventInterval, instance::setIdleEventInterval) + .acceptIfNotNull(this.failedDeclarationRetryInterval, instance::setFailedDeclarationRetryInterval) + .acceptIfNotNull(this.applicationEventPublisher, instance::setApplicationEventPublisher) + .acceptIfNotNull(this.autoStartup, instance::setAutoStartup) + .acceptIfNotNull(this.phase, instance::setPhase) + .acceptIfNotNull(this.afterReceivePostProcessors, instance::setAfterReceivePostProcessors) + .acceptIfNotNull(this.deBatchingEnabled, instance::setDeBatchingEnabled) + .acceptIfNotNull(this.messageAckListener, instance::setMessageAckListener) + .acceptIfNotNull(this.batchingStrategy, instance::setBatchingStrategy) + .acceptIfNotNull(getMicrometerEnabled(), instance::setMicrometerEnabled) + .acceptIfNotNull(getObservationEnabled(), instance::setObservationEnabled) + .acceptIfNotNull(this.observationConvention, instance::setObservationConvention) + .acceptIfNotNull(this.forceStop, instance::setForceStop); if (this.batchListener && this.deBatchingEnabled == null) { // turn off container debatching by default for batch listeners instance.setDeBatchingEnabled(false); } if (endpoint != null) { // endpoint settings overriding default factory settings javaUtils - .acceptIfNotNull(endpoint.getTaskExecutor(), instance::setTaskExecutor) - .acceptIfNotNull(endpoint.getAckMode(), instance::setAcknowledgeMode) - .acceptIfNotNull(endpoint.getBatchingStrategy(), instance::setBatchingStrategy); - instance.setListenerId(endpoint.getId()); + .acceptIfNotNull(endpoint.getTaskExecutor(), instance::setTaskExecutor) + .acceptIfNotNull(endpoint.getAckMode(), instance::setAcknowledgeMode) + .acceptIfNotNull(endpoint.getBatchingStrategy(), instance::setBatchingStrategy) + .acceptIfNotNull(endpoint.getId(), instance::setListenerId); if (endpoint.getBatchListener() == null) { endpoint.setBatchListener(this.batchListener); } @@ -428,7 +428,7 @@ public C createListenerContainer(RabbitListenerEndpoint endpoint) { * @param instance the container instance to configure. * @param endpoint the endpoint. */ - protected void initializeContainer(C instance, RabbitListenerEndpoint endpoint) { + protected void initializeContainer(C instance, @Nullable RabbitListenerEndpoint endpoint) { } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRetryOperationsInterceptorFactoryBean.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRetryOperationsInterceptorFactoryBean.java index c59fd91277..954c98cf0d 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRetryOperationsInterceptorFactoryBean.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRetryOperationsInterceptorFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -17,6 +17,7 @@ package org.springframework.amqp.rabbit.config; import org.aopalliance.aop.Advice; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.rabbit.retry.MessageRecoverer; import org.springframework.beans.factory.FactoryBean; @@ -30,9 +31,9 @@ */ public abstract class AbstractRetryOperationsInterceptorFactoryBean implements FactoryBean { - private MessageRecoverer messageRecoverer; + private @Nullable MessageRecoverer messageRecoverer; - private RetryOperations retryTemplate; + private @Nullable RetryOperations retryTemplate; public void setRetryOperations(RetryOperations retryTemplate) { this.retryTemplate = retryTemplate; @@ -42,11 +43,11 @@ public void setMessageRecoverer(MessageRecoverer messageRecoverer) { this.messageRecoverer = messageRecoverer; } - protected RetryOperations getRetryOperations() { + protected @Nullable RetryOperations getRetryOperations() { return this.retryTemplate; } - protected MessageRecoverer getMessageRecoverer() { + protected @Nullable MessageRecoverer getMessageRecoverer() { return this.messageRecoverer; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AnnotationDrivenParser.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AnnotationDrivenParser.java index cd0541fc5e..a2736c1d39 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AnnotationDrivenParser.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AnnotationDrivenParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -16,6 +16,7 @@ package org.springframework.amqp.rabbit.config; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.amqp.rabbit.annotation.RabbitListenerAnnotationBeanPostProcessor; @@ -34,12 +35,14 @@ * Parser for the 'annotation-driven' element of the 'rabbit' namespace. * * @author Stephane Nicoll + * @author Artem Bilan + * * @since 1.4 */ class AnnotationDrivenParser implements BeanDefinitionParser { @Override - public BeanDefinition parse(Element element, ParserContext parserContext) { + public @Nullable BeanDefinition parse(Element element, ParserContext parserContext) { Object source = parserContext.extractSource(element); // Register component for the surrounding element. @@ -85,7 +88,7 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { return null; } - private static void registerDefaultEndpointRegistry(Object source, ParserContext parserContext) { + private static void registerDefaultEndpointRegistry(@Nullable Object source, ParserContext parserContext) { BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(RabbitListenerEndpointRegistry.class); builder.getRawBeanDefinition().setSource(source); registerInfrastructureBean(parserContext, builder, diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BaseRabbitListenerContainerFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BaseRabbitListenerContainerFactory.java index 6363bbe18c..0e7178a169 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BaseRabbitListenerContainerFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BaseRabbitListenerContainerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 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. @@ -20,6 +20,7 @@ import java.util.function.Function; import org.aopalliance.aop.Advice; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.MessagePostProcessor; import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; @@ -32,7 +33,6 @@ import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; -import org.springframework.lang.Nullable; import org.springframework.retry.RecoveryCallback; import org.springframework.retry.support.RetryTemplate; import org.springframework.util.Assert; @@ -44,34 +44,37 @@ * * @author Gary Russell * @author Ngoc Nhan + * @author Artem Bilan + * * @since 2.4 * */ public abstract class BaseRabbitListenerContainerFactory - implements RabbitListenerContainerFactory, ApplicationContextAware { + implements RabbitListenerContainerFactory, ApplicationContextAware { - private Boolean defaultRequeueRejected; + private @Nullable Boolean defaultRequeueRejected; - private MessagePostProcessor[] beforeSendReplyPostProcessors; + private MessagePostProcessor @Nullable [] beforeSendReplyPostProcessors; - private RetryTemplate retryTemplate; + private @Nullable RetryTemplate retryTemplate; - private RecoveryCallback recoveryCallback; + private @Nullable RecoveryCallback recoveryCallback; - private Advice[] adviceChain; + private Advice @Nullable [] adviceChain; - private Function replyPostProcessorProvider; + private @Nullable Function<@Nullable String, @Nullable ReplyPostProcessor> replyPostProcessorProvider; - private Boolean micrometerEnabled; + private @Nullable Boolean micrometerEnabled; - private Boolean observationEnabled; + private @Nullable Boolean observationEnabled; + @SuppressWarnings("NullAway.Init") private ApplicationContext applicationContext; - private String beanName; + private @Nullable String beanName; @Override - public abstract C createListenerContainer(RabbitListenerEndpoint endpoint); + public abstract C createListenerContainer(@Nullable RabbitListenerEndpoint endpoint); /** * @param requeueRejected true to reject by default. @@ -85,7 +88,7 @@ public void setDefaultRequeueRejected(Boolean requeueRejected) { * Return the defaultRequeueRejected. * @return the defaultRequeueRejected. */ - protected Boolean getDefaultRequeueRejected() { + protected @Nullable Boolean getDefaultRequeueRejected() { return this.defaultRequeueRejected; } @@ -131,15 +134,18 @@ public void setReplyRecoveryCallback(RecoveryCallback recoveryCallback) { * @param replyPostProcessorProvider the post processor. * @since 3.0 */ - public void setReplyPostProcessorProvider(Function replyPostProcessorProvider) { + public void setReplyPostProcessorProvider( + Function<@Nullable String, @Nullable ReplyPostProcessor> replyPostProcessorProvider) { + this.replyPostProcessorProvider = replyPostProcessorProvider; } + @SuppressWarnings("NullAway") // Dataflow analysis limitation protected void applyCommonOverrides(@Nullable RabbitListenerEndpoint endpoint, C instance) { if (endpoint != null) { // endpoint settings overriding default factory settings JavaUtils.INSTANCE - .acceptIfNotNull(endpoint.getAutoStartup(), instance::setAutoStartup); - instance.setListenerId(endpoint.getId()); + .acceptIfNotNull(endpoint.getAutoStartup(), instance::setAutoStartup) + .acceptIfNotNull(endpoint.getId(), instance::setListenerId); endpoint.setupListenerContainer(instance); } Object iml = instance.getMessageListener(); @@ -169,8 +175,7 @@ protected void applyCommonOverrides(@Nullable RabbitListenerEndpoint endpoint, C * @return the advice chain that was set. Defaults to {@code null}. * @since 1.7.4 */ - @Nullable - public Advice[] getAdviceChain() { + public Advice @Nullable [] getAdviceChain() { return this.adviceChain == null ? null : Arrays.copyOf(this.adviceChain, this.adviceChain.length); } @@ -178,12 +183,12 @@ public Advice[] getAdviceChain() { * @param adviceChain the advice chain to set. * @see AbstractMessageListenerContainer#setAdviceChain */ - public void setAdviceChain(Advice... adviceChain) { + public void setAdviceChain(Advice @Nullable ... adviceChain) { this.adviceChain = adviceChain == null ? null : Arrays.copyOf(adviceChain, adviceChain.length); } /** - * Set to false to disable micrometer listener timers. When true, ignored + * Set to {@code false} to disable micrometer listener timers. When true, ignored * if {@link #setObservationEnabled(boolean)} is set to true. * @param micrometerEnabled false to disable. * @since 3.0 @@ -193,7 +198,7 @@ public void setMicrometerEnabled(boolean micrometerEnabled) { this.micrometerEnabled = micrometerEnabled; } - protected Boolean getMicrometerEnabled() { + protected @Nullable Boolean getMicrometerEnabled() { return this.micrometerEnabled; } @@ -208,7 +213,7 @@ public void setObservationEnabled(boolean observationEnabled) { this.observationEnabled = observationEnabled; } - protected Boolean getObservationEnabled() { + protected @Nullable Boolean getObservationEnabled() { return this.observationEnabled; } @@ -227,7 +232,7 @@ public void setBeanName(String name) { } @Override - public String getBeanName() { + public @Nullable String getBeanName() { return this.beanName; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BindingFactoryBean.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BindingFactoryBean.java index 1796d0adc4..a15bce3165 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BindingFactoryBean.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/BindingFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -18,37 +18,41 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.AmqpAdmin; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.Binding.DestinationType; import org.springframework.amqp.core.Exchange; import org.springframework.amqp.core.Queue; import org.springframework.beans.factory.FactoryBean; +import org.springframework.util.Assert; /** * @author Dave Syer * @author Gary Russell + * @author Artem Bilan * */ public class BindingFactoryBean implements FactoryBean { - private Map arguments; + private @Nullable Map arguments; private String routingKey = ""; - private String exchange; + private @Nullable String exchange; - private Queue destinationQueue; + private @Nullable Queue destinationQueue; - private Exchange destinationExchange; + private @Nullable Exchange destinationExchange; - private Boolean shouldDeclare; + private @Nullable Boolean shouldDeclare; - private Boolean ignoreDeclarationExceptions; + private @Nullable Boolean ignoreDeclarationExceptions; - private AmqpAdmin[] adminsThatShouldDeclare; + private AmqpAdmin @Nullable [] adminsThatShouldDeclare; - public void setArguments(Map arguments) { + public void setArguments(Map arguments) { this.arguments = arguments; } @@ -89,6 +93,7 @@ public Binding getObject() { destinationType = DestinationType.QUEUE; } else { + Assert.notNull(this.destinationExchange, "Or 'destinationExchange', or 'destinationQueue' must be provided"); destination = this.destinationExchange.getName(); destinationType = DestinationType.EXCHANGE; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/DirectRabbitListenerContainerFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/DirectRabbitListenerContainerFactory.java index 33b25be6f9..8d8413cb5d 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/DirectRabbitListenerContainerFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/DirectRabbitListenerContainerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 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. @@ -16,6 +16,8 @@ package org.springframework.amqp.rabbit.config; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.rabbit.listener.DirectMessageListenerContainer; import org.springframework.amqp.rabbit.listener.RabbitListenerEndpoint; import org.springframework.amqp.utils.JavaUtils; @@ -32,15 +34,15 @@ public class DirectRabbitListenerContainerFactory extends AbstractRabbitListenerContainerFactory { - private TaskScheduler taskScheduler; + private @Nullable TaskScheduler taskScheduler; - private Long monitorInterval; + private @Nullable Long monitorInterval; private Integer consumersPerQueue = 1; - private Integer messagesPerAck; + private @Nullable Integer messagesPerAck; - private Long ackTimeout; + private @Nullable Long ackTimeout; /** * Set the task scheduler to use for the task that monitors idle containers and @@ -53,7 +55,7 @@ public void setTaskScheduler(TaskScheduler taskScheduler) { /** * Set how often to run a task to check for failed consumers and idle containers. - * @param monitorInterval the interval; default 10000 but it will be adjusted down + * @param monitorInterval the interval; default 10000, but it will be adjusted down * to the smallest of this, {@link #setIdleEventInterval(Long) idleEventInterval} / 2 * (if configured) or * {@link #setFailedDeclarationRetryInterval(Long) failedDeclarationRetryInterval}. @@ -102,13 +104,15 @@ protected DirectMessageListenerContainer createContainerInstance() { } @Override - protected void initializeContainer(DirectMessageListenerContainer instance, RabbitListenerEndpoint endpoint) { + protected void initializeContainer(DirectMessageListenerContainer instance, + @Nullable RabbitListenerEndpoint endpoint) { + super.initializeContainer(instance, endpoint); JavaUtils javaUtils = JavaUtils.INSTANCE.acceptIfNotNull(this.taskScheduler, instance::setTaskScheduler) - .acceptIfNotNull(this.monitorInterval, instance::setMonitorInterval) - .acceptIfNotNull(this.messagesPerAck, instance::setMessagesPerAck) - .acceptIfNotNull(this.ackTimeout, instance::setAckTimeout); + .acceptIfNotNull(this.monitorInterval, instance::setMonitorInterval) + .acceptIfNotNull(this.messagesPerAck, instance::setMessagesPerAck) + .acceptIfNotNull(this.ackTimeout, instance::setAckTimeout); if (endpoint != null && endpoint.getConcurrency() != null) { try { instance.setConsumersPerQueue(Integer.parseInt(endpoint.getConcurrency())); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java index b2bc26fb8a..3f83b1a1e0 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 the original author or authors. + * Copyright 2016-2025 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. @@ -21,6 +21,7 @@ import java.util.concurrent.Executor; import org.aopalliance.aop.Advice; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.MessageListener; @@ -46,6 +47,7 @@ import org.springframework.scheduling.TaskScheduler; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.interceptor.TransactionAttribute; +import org.springframework.util.Assert; import org.springframework.util.ErrorHandler; import org.springframework.util.backoff.BackOff; @@ -66,123 +68,123 @@ public class ListenerContainerFactoryBean extends AbstractFactoryBean micrometerTags = new HashMap<>(); - private ApplicationContext applicationContext; + private @Nullable ApplicationContext applicationContext; - private String beanName; + private @Nullable String beanName; - private ApplicationEventPublisher applicationEventPublisher; + private @Nullable ApplicationEventPublisher applicationEventPublisher; private Type type = Type.simple; - private AbstractMessageListenerContainer listenerContainer; + private @Nullable AbstractMessageListenerContainer listenerContainer; - private ConnectionFactory connectionFactory; + private @Nullable ConnectionFactory connectionFactory; - private Boolean channelTransacted; + private @Nullable Boolean channelTransacted; - private AcknowledgeMode acknowledgeMode; + private @Nullable AcknowledgeMode acknowledgeMode; - private String[] queueNames; + private String @Nullable [] queueNames; - private Queue[] queues; + private Queue @Nullable [] queues; - private Boolean exposeListenerChannel; + private @Nullable Boolean exposeListenerChannel; - private MessageListener messageListener; + private @Nullable MessageListener messageListener; - private ErrorHandler errorHandler; + private @Nullable ErrorHandler errorHandler; - private Boolean deBatchingEnabled; + private @Nullable Boolean deBatchingEnabled; - private Advice[] adviceChain; + private Advice @Nullable [] adviceChain; - private MessagePostProcessor[] afterReceivePostProcessors; + private MessagePostProcessor @Nullable [] afterReceivePostProcessors; - private Boolean autoStartup; + private @Nullable Boolean autoStartup; - private Integer phase; + private @Nullable Integer phase; - private String listenerId; + private @Nullable String listenerId; - private ConsumerTagStrategy consumerTagStrategy; + private @Nullable ConsumerTagStrategy consumerTagStrategy; - private Map consumerArgs; + private @Nullable Map consumerArgs; - private Boolean noLocal; + private @Nullable Boolean noLocal; - private Boolean exclusive; + private @Nullable Boolean exclusive; - private Boolean defaultRequeueRejected; + private @Nullable Boolean defaultRequeueRejected; - private Integer prefetchCount; + private @Nullable Integer prefetchCount; - private Boolean globalQos; + private @Nullable Boolean globalQos; - private Long shutdownTimeout; + private @Nullable Long shutdownTimeout; - private Long idleEventInterval; + private @Nullable Long idleEventInterval; - private PlatformTransactionManager transactionManager; + private @Nullable PlatformTransactionManager transactionManager; - private TransactionAttribute transactionAttribute; + private @Nullable TransactionAttribute transactionAttribute; - private Executor taskExecutor; + private @Nullable Executor taskExecutor; - private Long recoveryInterval; + private @Nullable Long recoveryInterval; - private BackOff recoveryBackOff; + private @Nullable BackOff recoveryBackOff; - private MessagePropertiesConverter messagePropertiesConverter; + private @Nullable MessagePropertiesConverter messagePropertiesConverter; - private RabbitAdmin rabbitAdmin; + private @Nullable RabbitAdmin rabbitAdmin; - private Boolean missingQueuesFatal; + private @Nullable Boolean missingQueuesFatal; - private Boolean possibleAuthenticationFailureFatal; + private @Nullable Boolean possibleAuthenticationFailureFatal; - private Boolean mismatchedQueuesFatal; + private @Nullable Boolean mismatchedQueuesFatal; - private Boolean autoDeclare; + private @Nullable Boolean autoDeclare; - private Long failedDeclarationRetryInterval; + private @Nullable Long failedDeclarationRetryInterval; - private ConditionalExceptionLogger exclusiveConsumerExceptionLogger; + private @Nullable ConditionalExceptionLogger exclusiveConsumerExceptionLogger; - private Integer consumersPerQueue; + private @Nullable Integer consumersPerQueue; - private TaskScheduler taskScheduler; + private @Nullable TaskScheduler taskScheduler; - private Long monitorInterval; + private @Nullable Long monitorInterval; - private Integer concurrentConsumers; + private @Nullable Integer concurrentConsumers; - private Integer maxConcurrentConsumers; + private @Nullable Integer maxConcurrentConsumers; - private Long startConsumerMinInterval; + private @Nullable Long startConsumerMinInterval; - private Long stopConsumerMinInterval; + private @Nullable Long stopConsumerMinInterval; - private Integer consecutiveActiveTrigger; + private @Nullable Integer consecutiveActiveTrigger; - private Integer consecutiveIdleTrigger; + private @Nullable Integer consecutiveIdleTrigger; - private Long receiveTimeout; + private @Nullable Long receiveTimeout; - private Long batchReceiveTimeout; + private @Nullable Long batchReceiveTimeout; - private Integer batchSize; + private @Nullable Integer batchSize; - private Integer declarationRetries; + private @Nullable Integer declarationRetries; - private Long retryDeclarationInterval; + private @Nullable Long retryDeclarationInterval; - private Boolean consumerBatchEnabled; + private @Nullable Boolean consumerBatchEnabled; - private Boolean micrometerEnabled; + private @Nullable Boolean micrometerEnabled; - private ContainerCustomizer smlcCustomizer; + private @Nullable ContainerCustomizer smlcCustomizer; - private ContainerCustomizer dmlcCustomizer; + private @Nullable ContainerCustomizer dmlcCustomizer; @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { @@ -558,6 +560,7 @@ else if (this.dmlcCustomizer != null && this.type.equals(Type.direct)) { } private AbstractMessageListenerContainer createContainer() { + Assert.notNull(this.connectionFactory, "'connectionFactory' is required"); if (this.type.equals(Type.simple)) { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(this.connectionFactory); JavaUtils.INSTANCE diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerParser.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerParser.java index 71acf60981..b563e591c9 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerParser.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/ListenerContainerParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter; @@ -69,9 +70,9 @@ class ListenerContainerParser implements BeanDefinitionParser { private static final String EXCLUSIVE = "exclusive"; - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "NullAway"}) @Override - public BeanDefinition parse(Element element, ParserContext parserContext) { + public @Nullable BeanDefinition parse(Element element, ParserContext parserContext) { CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), parserContext.extractSource(element)); parserContext.pushContainingComponent(compositeDef); @@ -97,7 +98,7 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { parserContext.getReaderContext().error("Unexpected configuration for bean " + group, element); } containerList = (ManagedList) constructorArgumentValues - .getIndexedArgumentValue(0, ManagedList.class).getValue(); // NOSONAR never null + .getIndexedArgumentValue(0, ManagedList.class).getValue(); } List childElements = DomUtils.getChildElementsByTagName(element, LISTENER_ELEMENT); @@ -110,7 +111,8 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { } private void parseListener(Element listenerEle, Element containerEle, ParserContext parserContext, // NOSONAR complexity - ManagedList containerList) { + @Nullable ManagedList containerList) { + RootBeanDefinition listenerDef = new RootBeanDefinition(); listenerDef.setSource(parserContext.extractSource(listenerEle)); @@ -165,7 +167,7 @@ private void parseListener(Element listenerEle, Element containerEle, ParserCont String childElementId = listenerEle.getAttribute(ID_ATTRIBUTE); String containerBeanName = StringUtils.hasText(childElementId) ? childElementId : - BeanDefinitionReaderUtils.generateBeanName(containerDef, parserContext.getRegistry()); + BeanDefinitionReaderUtils.generateBeanName(containerDef, parserContext.getRegistry()); if (!NamespaceUtils.isAttributeDefined(listenerEle, QUEUE_NAMES_ATTRIBUTE) && !NamespaceUtils.isAttributeDefined(listenerEle, QUEUES_ATTRIBUTE)) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/NamespaceUtils.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/NamespaceUtils.java index 5b99acef0f..6ec9d21e0c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/NamespaceUtils.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/NamespaceUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -16,6 +16,7 @@ package org.springframework.amqp.rabbit.config; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.amqp.rabbit.support.ExpressionFactoryBean; @@ -43,8 +44,11 @@ public abstract class NamespaceUtils { public static final String BASE_PACKAGE = "org.springframework.amqp.core.rabbit.config"; + public static final String REF_ATTRIBUTE = "ref"; + public static final String METHOD_ATTRIBUTE = "method"; + public static final String ORDER = "order"; /** @@ -84,6 +88,7 @@ public static boolean setValueIfAttributeDefined(BeanDefinitionBuilder builder, */ public static boolean setValueIfAttributeDefined(BeanDefinitionBuilder builder, Element element, String attributeName) { + return setValueIfAttributeDefined(builder, element, attributeName, Conventions.attributeNameToPropertyName(attributeName)); } @@ -111,6 +116,7 @@ public static boolean isAttributeDefined(Element element, String attributeName) */ public static boolean addConstructorArgValueIfAttributeDefined(BeanDefinitionBuilder builder, Element element, String attributeName) { + String value = element.getAttribute(attributeName); if (StringUtils.hasText(value)) { builder.addConstructorArgValue(new TypedStringValue(value)); @@ -130,6 +136,7 @@ public static boolean addConstructorArgValueIfAttributeDefined(BeanDefinitionBui */ public static void addConstructorArgBooleanValueIfAttributeDefined(BeanDefinitionBuilder builder, Element element, String attributeName, boolean defaultValue) { + String value = element.getAttribute(attributeName); if (StringUtils.hasText(value)) { builder.addConstructorArgValue(new TypedStringValue(value)); @@ -151,6 +158,7 @@ public static void addConstructorArgBooleanValueIfAttributeDefined(BeanDefinitio */ public static boolean addConstructorArgRefIfAttributeDefined(BeanDefinitionBuilder builder, Element element, String attributeName) { + String value = element.getAttribute(attributeName); if (StringUtils.hasText(value)) { builder.addConstructorArgReference(value); @@ -171,6 +179,7 @@ public static boolean addConstructorArgRefIfAttributeDefined(BeanDefinitionBuild */ public static boolean addConstructorArgParentRefIfAttributeDefined(BeanDefinitionBuilder builder, Element element, String attributeName) { + String value = element.getAttribute(attributeName); if (StringUtils.hasText(value)) { BeanDefinitionBuilder child = BeanDefinitionBuilder.genericBeanDefinition(); @@ -194,6 +203,7 @@ public static boolean addConstructorArgParentRefIfAttributeDefined(BeanDefinitio */ public static boolean setReferenceIfAttributeDefined(BeanDefinitionBuilder builder, Element element, String attributeName, String propertyName) { + String attributeValue = element.getAttribute(attributeName); if (StringUtils.hasText(attributeValue)) { builder.addPropertyReference(propertyName, attributeValue); @@ -219,12 +229,13 @@ public static boolean setReferenceIfAttributeDefined(BeanDefinitionBuilder build */ public static boolean setReferenceIfAttributeDefined(BeanDefinitionBuilder builder, Element element, String attributeName) { + return setReferenceIfAttributeDefined(builder, element, attributeName, Conventions.attributeNameToPropertyName(attributeName)); } /** - * Provides a user friendly description of an element based on its node name and, if available, its "id" attribute + * Provides a user-friendly description of an element based on its node name and, if available, its "id" attribute * value. This is useful for creating error messages from within bean definition parsers. * * @param element The element. @@ -259,7 +270,7 @@ public static void parseDeclarationControls(Element element, BeanDefinitionBuild NamespaceUtils.setValueIfAttributeDefined(builder, element, "ignore-declaration-exceptions"); } - public static BeanDefinition createExpressionDefinitionFromValueOrExpression(String valueElementName, + public static @Nullable BeanDefinition createExpressionDefinitionFromValueOrExpression(String valueElementName, String expressionElementName, ParserContext parserContext, Element element, boolean oneRequired) { Assert.hasText(valueElementName, "'valueElementName' must not be empty"); @@ -291,7 +302,8 @@ public static BeanDefinition createExpressionDefinitionFromValueOrExpression(Str return expressionDef; } - public static BeanDefinition createExpressionDefIfAttributeDefined(String expressionElementName, Element element) { + public static @Nullable BeanDefinition createExpressionDefIfAttributeDefined( + String expressionElementName, Element element) { Assert.hasText(expressionElementName, "'expressionElementName' must no be empty"); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/QueueParser.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/QueueParser.java index 38314dad06..54196783ec 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/QueueParser.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/QueueParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -18,6 +18,7 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.amqp.core.AnonymousQueue; @@ -38,7 +39,7 @@ */ public class QueueParser extends AbstractSingleBeanDefinitionParser { - private static final ThreadLocal CURRENT_ELEMENT = new ThreadLocal<>(); + private static final ThreadLocal<@Nullable Element> CURRENT_ELEMENT = new ThreadLocal<>(); /** Element OR attribute. */ private static final String ARGUMENTS = "queue-arguments"; @@ -135,7 +136,7 @@ private void parseArguments(Element element, ParserContext parserContext, BeanDe Map map = parserContext.getDelegate().parseMapElement(argumentsElement, builder.getRawBeanDefinition()); if (StringUtils.hasText(ref)) { - if (map != null && !map.isEmpty()) { + if (!map.isEmpty()) { parserContext.getReaderContext() .error("You cannot have both a 'ref' and a nested map", element); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/RabbitNamespaceUtils.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/RabbitNamespaceUtils.java index c219c7bf7c..759e5a344b 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/RabbitNamespaceUtils.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/RabbitNamespaceUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -18,6 +18,7 @@ import java.util.List; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.amqp.core.AcknowledgeMode; @@ -357,7 +358,7 @@ public static BeanDefinition parseContainer(Element containerEle, ParserContext return containerDef; } - private static AcknowledgeMode parseAcknowledgeMode(Element ele, ParserContext parserContext) { + private static @Nullable AcknowledgeMode parseAcknowledgeMode(Element ele, ParserContext parserContext) { String acknowledge = ele.getAttribute(ACKNOWLEDGE_ATTRIBUTE); if (StringUtils.hasText(acknowledge)) { return switch (acknowledge) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/RetryInterceptorBuilder.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/RetryInterceptorBuilder.java index 4b32ee40ae..64ea7afa01 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/RetryInterceptorBuilder.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/RetryInterceptorBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 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. @@ -16,7 +16,10 @@ package org.springframework.amqp.rabbit.config; +import java.util.Objects; + import org.aopalliance.intercept.MethodInterceptor; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.rabbit.retry.MessageKeyGenerator; import org.springframework.amqp.rabbit.retry.MessageRecoverer; @@ -71,13 +74,13 @@ */ public abstract class RetryInterceptorBuilder, T extends MethodInterceptor> { - private RetryOperations retryOperations; + private @Nullable RetryOperations retryOperations; private final RetryTemplate retryTemplate = new RetryTemplate(); private final SimpleRetryPolicy simpleRetryPolicy = new SimpleRetryPolicy(); - private MessageRecoverer messageRecoverer; + private @Nullable MessageRecoverer messageRecoverer; private boolean templateAltered; @@ -198,17 +201,11 @@ protected void applyCommonSettings(AbstractRetryOperationsInterceptorFactoryBean if (this.messageRecoverer != null) { factoryBean.setMessageRecoverer(this.messageRecoverer); } - if (this.retryOperations != null) { - factoryBean.setRetryOperations(this.retryOperations); - } - else { - factoryBean.setRetryOperations(this.retryTemplate); - } + factoryBean.setRetryOperations(Objects.requireNonNullElse(this.retryOperations, this.retryTemplate)); } public abstract T build(); - /** * Builder for a stateful interceptor. */ @@ -218,9 +215,9 @@ public static final class StatefulRetryInterceptorBuilder private final StatefulRetryOperationsInterceptorFactoryBean factoryBean = new StatefulRetryOperationsInterceptorFactoryBean(); - private MessageKeyGenerator messageKeyGenerator; + private @Nullable MessageKeyGenerator messageKeyGenerator; - private NewMessageIdentifier newMessageIdentifier; + private @Nullable NewMessageIdentifier newMessageIdentifier; StatefulRetryInterceptorBuilder() { } @@ -260,7 +257,6 @@ public StatefulRetryOperationsInterceptor build() { } - /** * Builder for a stateless interceptor. */ diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerContainerFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerContainerFactory.java index c1739e540f..ba2e09bfa6 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerContainerFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerContainerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 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. @@ -16,6 +16,8 @@ package org.springframework.amqp.rabbit.config; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.rabbit.listener.RabbitListenerEndpoint; import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; import org.springframework.amqp.utils.JavaUtils; @@ -39,27 +41,27 @@ public class SimpleRabbitListenerContainerFactory extends AbstractRabbitListenerContainerFactory { - private Integer batchSize; + private @Nullable Integer batchSize; - private Integer concurrentConsumers; + private @Nullable Integer concurrentConsumers; - private Integer maxConcurrentConsumers; + private @Nullable Integer maxConcurrentConsumers; - private Long startConsumerMinInterval; + private @Nullable Long startConsumerMinInterval; - private Long stopConsumerMinInterval; + private @Nullable Long stopConsumerMinInterval; - private Integer consecutiveActiveTrigger; + private @Nullable Integer consecutiveActiveTrigger; - private Integer consecutiveIdleTrigger; + private @Nullable Integer consecutiveIdleTrigger; - private Long receiveTimeout; + private @Nullable Long receiveTimeout; - private Long batchReceiveTimeout; + private @Nullable Long batchReceiveTimeout; - private Boolean consumerBatchEnabled; + private @Nullable Boolean consumerBatchEnabled; - private Boolean enforceImmediateAckForManual; + private @Nullable Boolean enforceImmediateAckForManual; /** * @param batchSize the batch size. @@ -166,35 +168,38 @@ public void setConsumerBatchEnabled(boolean consumerBatchEnabled) { public void setEnforceImmediateAckForManual(Boolean enforceImmediateAckForManual) { this.enforceImmediateAckForManual = enforceImmediateAckForManual; } + @Override protected SimpleMessageListenerContainer createContainerInstance() { return new SimpleMessageListenerContainer(); } @Override - protected void initializeContainer(SimpleMessageListenerContainer instance, RabbitListenerEndpoint endpoint) { + protected void initializeContainer(SimpleMessageListenerContainer instance, + @Nullable RabbitListenerEndpoint endpoint) { + super.initializeContainer(instance, endpoint); JavaUtils javaUtils = JavaUtils.INSTANCE - .acceptIfNotNull(this.batchSize, instance::setBatchSize); + .acceptIfNotNull(this.batchSize, instance::setBatchSize); String concurrency = null; if (endpoint != null) { concurrency = endpoint.getConcurrency(); javaUtils.acceptIfNotNull(concurrency, instance::setConcurrency); } javaUtils - .acceptIfCondition(concurrency == null && this.concurrentConsumers != null, this.concurrentConsumers, - instance::setConcurrentConsumers) - .acceptIfCondition((concurrency == null || !(concurrency.contains("-"))) - && this.maxConcurrentConsumers != null, - this.maxConcurrentConsumers, instance::setMaxConcurrentConsumers) - .acceptIfNotNull(this.startConsumerMinInterval, instance::setStartConsumerMinInterval) - .acceptIfNotNull(this.stopConsumerMinInterval, instance::setStopConsumerMinInterval) - .acceptIfNotNull(this.consecutiveActiveTrigger, instance::setConsecutiveActiveTrigger) - .acceptIfNotNull(this.consecutiveIdleTrigger, instance::setConsecutiveIdleTrigger) - .acceptIfNotNull(this.receiveTimeout, instance::setReceiveTimeout) - .acceptIfNotNull(this.batchReceiveTimeout, instance::setBatchReceiveTimeout) - .acceptIfNotNull(this.enforceImmediateAckForManual, instance::setEnforceImmediateAckForManual); + .acceptIfCondition(concurrency == null && this.concurrentConsumers != null, this.concurrentConsumers, + instance::setConcurrentConsumers) + .acceptIfCondition((concurrency == null || !(concurrency.contains("-"))) + && this.maxConcurrentConsumers != null, + this.maxConcurrentConsumers, instance::setMaxConcurrentConsumers) + .acceptIfNotNull(this.startConsumerMinInterval, instance::setStartConsumerMinInterval) + .acceptIfNotNull(this.stopConsumerMinInterval, instance::setStopConsumerMinInterval) + .acceptIfNotNull(this.consecutiveActiveTrigger, instance::setConsecutiveActiveTrigger) + .acceptIfNotNull(this.consecutiveIdleTrigger, instance::setConsecutiveIdleTrigger) + .acceptIfNotNull(this.receiveTimeout, instance::setReceiveTimeout) + .acceptIfNotNull(this.batchReceiveTimeout, instance::setBatchReceiveTimeout) + .acceptIfNotNull(this.enforceImmediateAckForManual, instance::setEnforceImmediateAckForManual); if (Boolean.TRUE.equals(this.consumerBatchEnabled)) { instance.setConsumerBatchEnabled(true); /* diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerEndpoint.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerEndpoint.java index c1dcfd6f10..ff7035257c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerEndpoint.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/SimpleRabbitListenerEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 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. @@ -17,6 +17,8 @@ package org.springframework.amqp.rabbit.config; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.rabbit.listener.AbstractRabbitListenerEndpoint; import org.springframework.amqp.rabbit.listener.MessageListenerContainer; @@ -31,7 +33,7 @@ */ public class SimpleRabbitListenerEndpoint extends AbstractRabbitListenerEndpoint { - private MessageListener messageListener; + private @Nullable MessageListener messageListener; /** @@ -47,13 +49,13 @@ public void setMessageListener(MessageListener messageListener) { * @return the {@link MessageListener} to invoke when a message matching * the endpoint is received. */ - public MessageListener getMessageListener() { + public @Nullable MessageListener getMessageListener() { return this.messageListener; } @Override - protected MessageListener createMessageListener(MessageListenerContainer container) { + protected @Nullable MessageListener createMessageListener(MessageListenerContainer container) { return getMessageListener(); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatefulRetryOperationsInterceptorFactoryBean.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatefulRetryOperationsInterceptorFactoryBean.java index 4e79caaa98..4285b39df4 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatefulRetryOperationsInterceptorFactoryBean.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatefulRetryOperationsInterceptorFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -20,6 +20,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.ImmediateAcknowledgeAmqpException; import org.springframework.amqp.core.Message; @@ -27,7 +28,6 @@ import org.springframework.amqp.rabbit.retry.MessageKeyGenerator; import org.springframework.amqp.rabbit.retry.MessageRecoverer; import org.springframework.amqp.rabbit.retry.NewMessageIdentifier; -import org.springframework.lang.Nullable; import org.springframework.retry.RetryOperations; import org.springframework.retry.interceptor.MethodArgumentsKeyGenerator; import org.springframework.retry.interceptor.MethodInvocationRecoverer; @@ -50,6 +50,7 @@ * @author Dave Syer * @author Gary Russell * @author Ngoc Nhan + * @author Artem Bilan * * @see RetryOperations#execute(org.springframework.retry.RetryCallback, org.springframework.retry.RecoveryCallback, * org.springframework.retry.RetryState) @@ -57,11 +58,11 @@ */ public class StatefulRetryOperationsInterceptorFactoryBean extends AbstractRetryOperationsInterceptorFactoryBean { - private static Log logger = LogFactory.getLog(StatefulRetryOperationsInterceptorFactoryBean.class); + private static final Log LOGGER = LogFactory.getLog(StatefulRetryOperationsInterceptorFactoryBean.class); - private MessageKeyGenerator messageKeyGenerator; + private @Nullable MessageKeyGenerator messageKeyGenerator; - private NewMessageIdentifier newMessageIdentifier; + private @Nullable NewMessageIdentifier newMessageIdentifier; public void setMessageKeyGenerator(MessageKeyGenerator messageKeyGenerator) { this.messageKeyGenerator = messageKeyGenerator; @@ -90,8 +91,9 @@ public StatefulRetryOperationsInterceptor getObject() { private NewMethodArgumentsIdentifier createNewItemIdentifier() { return args -> { Message message = argToMessage(args); + Assert.notNull(message, "The 'args' must not convert to null"); if (StatefulRetryOperationsInterceptorFactoryBean.this.newMessageIdentifier == null) { - return !message.getMessageProperties().isRedelivered(); + return Boolean.FALSE.equals(message.getMessageProperties().isRedelivered()); } return StatefulRetryOperationsInterceptorFactoryBean.this.newMessageIdentifier.isNew(message); @@ -104,7 +106,7 @@ private MethodInvocationRecoverer createRecoverer() { MessageRecoverer messageRecoverer = getMessageRecoverer(); Object arg = args[1]; if (messageRecoverer == null) { - logger.warn("Message(s) dropped on recovery: " + arg, cause); + LOGGER.warn("Message(s) dropped on recovery: " + arg, cause); } else if (arg instanceof Message msg) { messageRecoverer.recover(msg, cause); @@ -125,7 +127,7 @@ private MethodArgumentsKeyGenerator createKeyGenerator() { Assert.notNull(message, "The 'args' must not convert to null"); if (StatefulRetryOperationsInterceptorFactoryBean.this.messageKeyGenerator == null) { String messageId = message.getMessageProperties().getMessageId(); - if (messageId == null && message.getMessageProperties().isRedelivered()) { + if (messageId == null && Boolean.TRUE.equals(message.getMessageProperties().isRedelivered())) { message.getMessageProperties().setFinalRetryForMessageWithNoId(true); } return messageId; @@ -134,8 +136,7 @@ private MethodArgumentsKeyGenerator createKeyGenerator() { }; } - @Nullable - private Message argToMessage(Object[] args) { + private @Nullable Message argToMessage(Object[] args) { Object arg = args[1]; if (arg instanceof Message msg) { return msg; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatelessRetryOperationsInterceptorFactoryBean.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatelessRetryOperationsInterceptorFactoryBean.java index 17288ec683..9fb0a4635b 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatelessRetryOperationsInterceptorFactoryBean.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/StatelessRetryOperationsInterceptorFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -20,6 +20,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.retry.MessageBatchRecoverer; @@ -36,7 +37,7 @@ * if your listener can be called repeatedly between failures with no side effects. The semantics of stateless retry * mean that a listener exception is not propagated to the container until the retry attempts are exhausted. When the * retry attempts are exhausted it can be processed using a {@link MessageRecoverer} if one is provided, in the same - * transaction (in which case no exception is propagated). If a recoverer is not provided the exception will be + * transaction (in which case no exception is propagated). If a recoverer is not provided, the exception will be * propagated and the message may be redelivered if the channel is transactional. * * @author Dave Syer @@ -62,13 +63,12 @@ public RetryOperationsInterceptor getObject() { } - @SuppressWarnings("unchecked") protected MethodInvocationRecoverer createRecoverer() { return this::recover; } @SuppressWarnings("unchecked") - protected Object recover(Object[] args, Throwable cause) { + protected @Nullable Object recover(Object[] args, Throwable cause) { MessageRecoverer messageRecoverer = getMessageRecoverer(); Object arg = args[1]; if (messageRecoverer == null) { @@ -88,9 +88,4 @@ public Class getObjectType() { return RetryOperationsInterceptor.class; } - @Override - public boolean isSingleton() { - return true; - } - } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/TemplateParser.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/TemplateParser.java index c9c5c3f908..372471ea84 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/TemplateParser.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/TemplateParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -167,12 +167,10 @@ protected void doParse(Element element, ParserContext parserContext, BeanDefinit if (childElement != null) { replyContainer = parseListener(childElement, element, parserContext); - if (replyContainer != null) { - replyContainer.getPropertyValues().add("messageListener", - new RuntimeBeanReference(element.getAttribute(ID_ATTRIBUTE))); - String replyContainerName = element.getAttribute(ID_ATTRIBUTE) + ".replyListener"; - parserContext.getRegistry().registerBeanDefinition(replyContainerName, replyContainer); - } + replyContainer.getPropertyValues().add("messageListener", + new RuntimeBeanReference(element.getAttribute(ID_ATTRIBUTE))); + String replyContainerName = element.getAttribute(ID_ATTRIBUTE) + ".replyListener"; + parserContext.getRegistry().registerBeanDefinition(replyContainerName, replyContainer); } if (replyContainer == null && element.hasAttribute(REPLY_QUEUE_ATTRIBUTE)) { parserContext.getReaderContext().error( @@ -193,11 +191,9 @@ else if (replyContainer != null && !element.hasAttribute(REPLY_QUEUE_ATTRIBUTE)) private BeanDefinition parseListener(Element childElement, Element element, ParserContext parserContext) { BeanDefinition replyContainer = RabbitNamespaceUtils.parseContainer(childElement, parserContext); - if (replyContainer != null) { - replyContainer.getPropertyValues().add( - "connectionFactory", - new RuntimeBeanReference(element.getAttribute(CONNECTION_FACTORY_ATTRIBUTE))); - } + replyContainer.getPropertyValues().add( + "connectionFactory", + new RuntimeBeanReference(element.getAttribute(CONNECTION_FACTORY_ATTRIBUTE))); if (element.hasAttribute(REPLY_QUEUE_ATTRIBUTE)) { replyContainer.getPropertyValues().add("queues", new RuntimeBeanReference(element.getAttribute(REPLY_QUEUE_ATTRIBUTE))); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/package-info.java index 7d63279049..aea20aa616 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/package-info.java @@ -1,4 +1,5 @@ /** * Provides classes supporting the Rabbit XML namespace. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.config; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java index a901c01100..281906e38a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractConnectionFactory.java @@ -46,6 +46,7 @@ import com.rabbitmq.client.impl.recovery.AutorecoveringConnection; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.amqp.support.ConditionalExceptionLogger; @@ -57,7 +58,6 @@ import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextClosedEvent; -import org.springframework.lang.Nullable; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -120,7 +120,7 @@ public enum AddressShuffleMode { private ConditionalExceptionLogger closeExceptionLogger = new DefaultChannelCloseLogger(); - private AbstractConnectionFactory publisherConnectionFactory; + private @Nullable AbstractConnectionFactory publisherConnectionFactory; private RecoveryListener recoveryListener = new RecoveryListener() { @@ -140,9 +140,9 @@ public void handleRecovery(Recoverable recoverable) { }; - private ExecutorService executorService; + private @Nullable ExecutorService executorService; - private List

addresses; + private @Nullable List
addresses; private AddressShuffleMode addressShuffleMode = AddressShuffleMode.RANDOM; @@ -153,18 +153,18 @@ public void handleRecovery(Recoverable recoverable) { "#" + ObjectUtils.getIdentityHexString(this) + ":" + this.defaultConnectionNameStrategyCounter.getAndIncrement(); - private String beanName; + private @Nullable String beanName; + @SuppressWarnings("NullAway.Init") private ApplicationContext applicationContext; - private ApplicationEventPublisher applicationEventPublisher; + private @Nullable ApplicationEventPublisher applicationEventPublisher; - private AddressResolver addressResolver; + private @Nullable AddressResolver addressResolver; private volatile boolean contextStopped; - @Nullable - private BackOff connectionCreatingBackOff; + private @Nullable BackOff connectionCreatingBackOff; /** * Create a new AbstractConnectionFactory for the given target ConnectionFactory, with no publisher connection @@ -213,7 +213,7 @@ public void setApplicationEventPublisher(ApplicationEventPublisher applicationEv } } - protected ApplicationEventPublisher getApplicationEventPublisher() { + protected @Nullable ApplicationEventPublisher getApplicationEventPublisher() { return this.applicationEventPublisher; } @@ -241,8 +241,8 @@ public com.rabbitmq.client.ConnectionFactory getRabbitConnectionFactory() { } /** - * Return the user name from the underlying rabbit connection factory. - * @return the user name. + * Return the username from the underlying rabbit connection factory. + * @return the username. * @since 1.6 */ @Override @@ -376,8 +376,7 @@ public void setAddresses(String addresses) { } } - @Nullable - protected List
getAddresses() throws IOException { + protected @Nullable List
getAddresses() throws IOException { this.lock.lock(); try { return this.addressResolver != null ? this.addressResolver.getAddresses() : this.addresses; @@ -481,8 +480,7 @@ public void setExecutor(Executor executor) { } } - @Nullable - protected ExecutorService getExecutorService() { + protected @Nullable ExecutorService getExecutorService() { return this.executorService; } @@ -550,8 +548,7 @@ public void setBeanName(String name) { * @return the bean name or null. * @since 1.7.9 */ - @Nullable - protected String getBeanName() { + protected @Nullable String getBeanName() { return this.beanName; } @@ -583,7 +580,7 @@ public void setConnectionCreatingBackOff(@Nullable BackOff backOff) { } @Override - public ConnectionFactory getPublisherConnectionFactory() { + public @Nullable ConnectionFactory getPublisherConnectionFactory() { return this.publisherConnectionFactory; } @@ -617,7 +614,7 @@ public void handleRecovery(Recoverable recoverable) { if (this.logger.isInfoEnabled()) { this.logger.info("Created new connection: " + connectionName + "/" + connection); } - if (this.recoveryListener != null && rabbitConnection instanceof AutorecoveringConnection auto) { + if (rabbitConnection instanceof AutorecoveringConnection auto) { auto.addRecoveryListener(this.recoveryListener); } @@ -663,6 +660,7 @@ private com.rabbitmq.client.Connection connectResolver(String connectionName) th connectionName); } + @SuppressWarnings("NullAway") // Dataflow analysis limitation private com.rabbitmq.client.Connection connectAddresses(String connectionName) throws IOException, TimeoutException { @@ -764,7 +762,7 @@ public void handleUnblocked() { public static class DefaultChannelCloseLogger implements ConditionalExceptionLogger { @Override - public void log(Log logger, String message, Throwable t) { + public void log(Log logger, String message, @Nullable Throwable t) { if (t instanceof ShutdownSignalException cause) { if (RabbitUtils.isPassiveDeclarationChannelClose(cause)) { if (logger.isDebugEnabled()) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.java index be70f77cc5..01399efc67 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AbstractRoutingConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -21,10 +21,11 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.AmqpException; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -47,13 +48,13 @@ public abstract class AbstractRoutingConnectionFactory implements ConnectionFact private final List connectionListeners = new ArrayList<>(); - private ConnectionFactory defaultTargetConnectionFactory; + private @Nullable ConnectionFactory defaultTargetConnectionFactory; private boolean lenientFallback = true; - private Boolean confirms; + private @Nullable Boolean confirms; - private Boolean returns; + private @Nullable Boolean returns; private boolean consistentConfirmsReturns = true; @@ -108,12 +109,12 @@ public boolean isLenientFallback() { @Override public boolean isPublisherConfirms() { - return this.confirms; + return Boolean.TRUE.equals(this.confirms); } @Override public boolean isPublisherReturns() { - return this.returns; + return Boolean.TRUE.equals(this.returns); } @Override @@ -130,9 +131,9 @@ private void checkConfirmsAndReturns(ConnectionFactory cf) { } if (this.consistentConfirmsReturns) { - Assert.isTrue(this.confirms.booleanValue() == cf.isPublisherConfirms(), + Assert.isTrue(this.confirms == cf.isPublisherConfirms(), "Target connection factories must have the same setting for publisher confirms"); - Assert.isTrue(this.returns.booleanValue() == cf.isPublisherReturns(), + Assert.isTrue(this.returns == cf.isPublisherReturns(), "Target connection factories must have the same setting for publisher returns"); } } @@ -212,27 +213,27 @@ public void clearConnectionListeners() { } @Override - public String getHost() { - return this.determineTargetConnectionFactory().getHost(); + public @Nullable String getHost() { + return determineTargetConnectionFactory().getHost(); } @Override public int getPort() { - return this.determineTargetConnectionFactory().getPort(); + return determineTargetConnectionFactory().getPort(); } @Override public String getVirtualHost() { - return this.determineTargetConnectionFactory().getVirtualHost(); + return determineTargetConnectionFactory().getVirtualHost(); } @Override public String getUsername() { - return this.determineTargetConnectionFactory().getUsername(); + return determineTargetConnectionFactory().getUsername(); } @Override - public ConnectionFactory getTargetConnectionFactory(Object key) { + public @Nullable ConnectionFactory getTargetConnectionFactory(Object key) { return this.targetConnectionFactories.get(key); } @@ -283,8 +284,7 @@ protected ConnectionFactory removeTargetConnectionFactory(Object key) { * * @return The lookup key. */ - @Nullable - protected abstract Object determineCurrentLookupKey(); + protected abstract @Nullable Object determineCurrentLookupKey(); @Override public void destroy() { @@ -294,7 +294,9 @@ public void destroy() { @Override public void resetConnection() { this.targetConnectionFactories.values().forEach(ConnectionFactory::resetConnection); - this.defaultTargetConnectionFactory.resetConnection(); + if (this.defaultTargetConnectionFactory != null) { + this.defaultTargetConnectionFactory.resetConnection(); + } } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AfterCompletionFailedException.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AfterCompletionFailedException.java index 259f0d5958..18b507f959 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AfterCompletionFailedException.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/AfterCompletionFailedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2025 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. @@ -16,6 +16,8 @@ package org.springframework.amqp.rabbit.connection; +import java.io.Serial; + import org.springframework.amqp.AmqpException; /** @@ -27,6 +29,7 @@ */ public class AfterCompletionFailedException extends AmqpException { + @Serial private static final long serialVersionUID = 1L; private final int syncStatus; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java index a160896b8c..8ee1f34076 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactory.java @@ -56,6 +56,7 @@ import com.rabbitmq.client.ShutdownListener; import com.rabbitmq.client.ShutdownSignalException; import com.rabbitmq.client.impl.recovery.AutorecoveringChannel; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpApplicationContextClosedException; import org.springframework.amqp.AmqpException; @@ -65,7 +66,6 @@ import org.springframework.context.SmartLifecycle; import org.springframework.jmx.export.annotation.ManagedAttribute; import org.springframework.jmx.export.annotation.ManagedResource; -import org.springframework.lang.Nullable; import org.springframework.scheduling.concurrent.CustomizableThreadFactory; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -223,7 +223,7 @@ public enum ConfirmType { /** * Executor used for channels if no explicit executor set. */ - private volatile ExecutorService channelsExecutor; + private volatile @Nullable ExecutorService channelsExecutor; private volatile boolean stopped; @@ -258,6 +258,7 @@ public CachingConnectionFactory(int port) { * @param hostNameArg the host name to connect to * @param port the port number */ + @SuppressWarnings("this-escape") public CachingConnectionFactory(@Nullable String hostNameArg, int port) { super(newRabbitConnectionFactory()); String hostname = hostNameArg; @@ -274,6 +275,7 @@ public CachingConnectionFactory(@Nullable String hostNameArg, int port) { * @param uri the amqp uri configuring the connection * @since 1.5 */ + @SuppressWarnings("this-escape") public CachingConnectionFactory(URI uri) { super(newRabbitConnectionFactory()); setUri(uri); @@ -293,6 +295,7 @@ public CachingConnectionFactory(com.rabbitmq.client.ConnectionFactory rabbitConn * @param rabbitConnectionFactory the target ConnectionFactory * @param isPublisherFactory true if this is the publisher sub-factory. */ + @SuppressWarnings("this-escape") private CachingConnectionFactory(com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory, boolean isPublisherFactory) { @@ -338,6 +341,7 @@ public void setPublisherConnectionFactory(@Nullable AbstractConnectionFactory pu * @param sessionCacheSize the channel cache size. * @see #setChannelCheckoutTimeout(long) */ + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void setChannelCacheSize(int sessionCacheSize) { Assert.isTrue(sessionCacheSize >= 1, "Channel cache size must be 1 or higher"); this.channelCacheSize = sessionCacheSize; @@ -354,6 +358,7 @@ public CacheMode getCacheMode() { return this.cacheMode; } + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void setCacheMode(CacheMode cacheMode) { Assert.isTrue(!this.initialized, "'cacheMode' cannot be changed after initialization."); Assert.notNull(cacheMode, "'cacheMode' must not be null."); @@ -367,6 +372,7 @@ public int getConnectionCacheSize() { return this.connectionCacheSize; } + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void setConnectionCacheSize(int connectionCacheSize) { Assert.isTrue(connectionCacheSize >= 1, "Connection cache size must be 1 or higher."); this.connectionCacheSize = connectionCacheSize; @@ -383,6 +389,7 @@ public void setConnectionCacheSize(int connectionCacheSize) { * @param connectionLimit the limit. * @since 1.5.5 */ + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void setConnectionLimit(int connectionLimit) { Assert.isTrue(connectionLimit >= 1, "Connection limit must be 1 or higher."); this.connectionLimit = connectionLimit; @@ -401,6 +408,7 @@ public boolean isPublisherReturns() { return this.publisherReturns; } + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void setPublisherReturns(boolean publisherReturns) { this.publisherReturns = publisherReturns; if (this.defaultPublisherFactory) { @@ -418,6 +426,7 @@ public boolean isSimplePublisherConfirms() { * @param confirmType the confirm type. * @since 2.2 */ + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void setPublisherConfirmType(ConfirmType confirmType) { Assert.notNull(confirmType, "'confirmType' cannot be null"); this.confirmType = confirmType; @@ -438,11 +447,12 @@ public void setPublisherConfirmType(ConfirmType confirmType) { * @since 1.4.2 * @see #setConnectionLimit(int) */ + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void setChannelCheckoutTimeout(long channelCheckoutTimeout) { this.channelCheckoutTimeout = channelCheckoutTimeout; if (this.defaultPublisherFactory) { ((CachingConnectionFactory) getPublisherConnectionFactory()) - .setChannelCheckoutTimeout(channelCheckoutTimeout); // NOSONAR + .setChannelCheckoutTimeout(channelCheckoutTimeout); } } @@ -462,6 +472,7 @@ public int getPhase() { } @Override + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void afterPropertiesSet() { this.initialized = true; if (this.cacheMode == CacheMode.CHANNEL) { @@ -677,12 +688,19 @@ private Channel createBareChannel(ChannelCachingConnectionProxy connection, bool } return doCreateBareChannel(this.connection, transactional); } - else if (this.cacheMode == CacheMode.CONNECTION) { + else { if (!connection.isOpen()) { this.connectionLock.lock(); try { - this.allocatedConnectionNonTransactionalChannels.get(connection).clear(); - this.allocatedConnectionTransactionalChannels.get(connection).clear(); + LinkedList channelProxies = + this.allocatedConnectionNonTransactionalChannels.get(connection); + if (channelProxies != null) { + channelProxies.clear(); + } + channelProxies = this.allocatedConnectionTransactionalChannels.get(connection); + if (channelProxies != null) { + channelProxies.clear(); + } connection.notifyCloseIfNecessary(); refreshProxyConnection(connection); } @@ -692,7 +710,6 @@ else if (this.cacheMode == CacheMode.CONNECTION) { } return doCreateBareChannel(connection, transactional); } - return null; // NOSONAR doCreate will throw an exception } private Channel doCreateBareChannel(ChannelCachingConnectionProxy conn, boolean transactional) { @@ -721,26 +738,23 @@ public final Connection createConnection() throws AmqpException { } this.connectionLock.lock(); try { - if (this.cacheMode == CacheMode.CHANNEL) { - if (this.connection.target == null) { - this.connection.target = super.createBareConnection(); - // invoke the listener *after* this.connection is assigned - if (!this.checkoutPermits.containsKey(this.connection)) { - this.checkoutPermits.put(this.connection, new Semaphore(this.channelCacheSize)); - } - this.connection.closeNotified.set(false); - getConnectionListener().onCreate(this.connection); - } - return this.connection; - } - else if (this.cacheMode == CacheMode.CONNECTION) { + if (this.cacheMode == CacheMode.CONNECTION) { return connectionFromCache(); } + if (this.connection.target == null) { + this.connection.target = super.createBareConnection(); + // invoke the listener *after* this.connection is assigned + if (!this.checkoutPermits.containsKey(this.connection)) { + this.checkoutPermits.put(this.connection, new Semaphore(this.channelCacheSize)); + } + this.connection.closeNotified.set(false); + getConnectionListener().onCreate(this.connection); + } + return this.connection; } finally { this.connectionLock.unlock(); } - return null; // NOSONAR - never reach here - exceptions } private Connection connectionFromCache() { @@ -810,8 +824,7 @@ private ChannelCachingConnectionProxy waitForConnection(long now) { * return null, if there are no open idle, return the first closed idle so it can * be reopened. */ - @Nullable - private ChannelCachingConnectionProxy findIdleConnection() { + private @Nullable ChannelCachingConnectionProxy findIdleConnection() { ChannelCachingConnectionProxy cachedConnection = null; ChannelCachingConnectionProxy lastIdle = this.idleConnections.peekLast(); while (cachedConnection == null) { @@ -865,14 +878,15 @@ public final void destroy() { this.stopped = true; this.connectionLock.lock(); try { - if (this.channelsExecutor != null) { + ExecutorService executorService = this.channelsExecutor; + if (executorService != null) { try { if (!this.inFlightAsyncCloses.await(CHANNEL_EXEC_SHUTDOWN_TIMEOUT, TimeUnit.SECONDS)) { this.logger .warn("Async closes are still in-flight: " + this.inFlightAsyncCloses.getCount()); } - this.channelsExecutor.shutdown(); - if (!this.channelsExecutor.awaitTermination(CHANNEL_EXEC_SHUTDOWN_TIMEOUT, TimeUnit.SECONDS)) { + executorService.shutdown(); + if (!executorService.awaitTermination(CHANNEL_EXEC_SHUTDOWN_TIMEOUT, TimeUnit.SECONDS)) { this.logger.warn("Channel executor failed to shut down"); } } @@ -899,6 +913,7 @@ public final void destroy() { * broker. */ @Override + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void resetConnection() { this.connectionLock.lock(); try { @@ -948,6 +963,7 @@ protected void closeChannels(Collection theChannels) { } @ManagedAttribute + @SuppressWarnings("NullAway") // Dataflow analysis limitation public Properties getCacheProperties() { Properties props = new Properties(); props.setProperty("cacheMode", this.cacheMode.name()); @@ -1007,6 +1023,7 @@ public Properties getCacheProperties() { * @since 2.0.2 */ @ManagedAttribute + @SuppressWarnings("NullAway") // Dataflow analysis limitation public Properties getPublisherConnectionFactoryCacheProperties() { if (this.defaultPublisherFactory) { return ((CachingConnectionFactory) getPublisherConnectionFactory()).getCacheProperties(); // NOSONAR @@ -1015,14 +1032,12 @@ public Properties getPublisherConnectionFactoryCacheProperties() { } private void putConnectionName(Properties props, ConnectionProxy connection, String keySuffix) { - Connection targetConnection = connection.getTargetConnection(); // NOSONAR (close()) + Connection targetConnection = connection.getTargetConnection(); if (targetConnection != null) { com.rabbitmq.client.Connection delegate = targetConnection.getDelegate(); - if (delegate != null) { - String name = delegate.getClientProvidedName(); - if (name != null) { - props.put("connectionName" + keySuffix, name); - } + String name = delegate.getClientProvidedName(); + if (name != null) { + props.put("connectionName" + keySuffix, name); } } } @@ -1043,26 +1058,29 @@ private int countOpenConnections() { * @since 1.7.9 */ protected ExecutorService getChannelsExecutor() { - if (getExecutorService() != null) { - return getExecutorService(); // NOSONAR never null - } - if (this.channelsExecutor == null) { - this.connectionLock.lock(); - try { - if (this.channelsExecutor == null) { - final String threadPrefix = - getBeanName() == null - ? DEFAULT_DEFERRED_POOL_PREFIX + threadPoolId.incrementAndGet() - : getBeanName(); - ThreadFactory threadPoolFactory = new CustomizableThreadFactory(threadPrefix); // NOSONAR never null - this.channelsExecutor = Executors.newCachedThreadPool(threadPoolFactory); + ExecutorService executorService = getExecutorService(); + if (executorService == null) { + executorService = this.channelsExecutor; + if (executorService == null) { + this.connectionLock.lock(); + try { + executorService = this.channelsExecutor; + if (executorService == null) { + final String threadPrefix = + getBeanName() == null + ? DEFAULT_DEFERRED_POOL_PREFIX + threadPoolId.incrementAndGet() + : getBeanName(); + ThreadFactory threadPoolFactory = new CustomizableThreadFactory(threadPrefix); // NOSONAR never null + executorService = Executors.newCachedThreadPool(threadPoolFactory); + this.channelsExecutor = executorService; + } + } + finally { + this.connectionLock.unlock(); } - } - finally { - this.connectionLock.unlock(); } } - return this.channelsExecutor; + return executorService; } @Override @@ -1102,7 +1120,7 @@ private final class CachedChannelInvocationHandler implements InvocationHandler private final boolean publisherConfirms = ConfirmType.CORRELATED.equals(CachingConnectionFactory.this.confirmType); - private volatile Channel target; + private volatile @Nullable Channel target; private volatile boolean txStarted; @@ -1119,7 +1137,7 @@ private final class CachedChannelInvocationHandler implements InvocationHandler } @Override // NOSONAR complexity - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // NOSONAR NCSS lines + public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // NOSONAR NCSS lines ChannelProxy channelProxy = (ChannelProxy) proxy; if (logger.isTraceEnabled() && !method.getName().equals("toString") && !method.getName().equals("hashCode") && !method.getName().equals("equals")) { @@ -1150,12 +1168,11 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl // Handle close method: don't pass the call on. if (CachingConnectionFactory.this.active && !RabbitUtils.isPhysicalCloseRequired()) { logicalClose(channelProxy); - return null; } else { physicalClose(); - return null; } + return null; } case "getTargetChannel" -> { // Handle getTargetChannel method: return underlying Channel. @@ -1163,7 +1180,8 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl } case "isOpen" -> { // Handle isOpen method: we are closed if the target is closed - return this.target != null && this.target.isOpen(); + Channel targetToCheck = this.target; + return targetToCheck != null && targetToCheck.isOpen(); } case "isTransactional" -> { return this.transactional; @@ -1176,9 +1194,10 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl } } try { - if (this.target == null || !this.target.isOpen()) { - if (this.target instanceof PublisherCallbackChannel) { - this.target.close(); + Channel targetChannel = this.target; + if (targetChannel == null || !targetChannel.isOpen()) { + if (targetChannel instanceof PublisherCallbackChannel) { + targetChannel.close(); throw new InvocationTargetException( new AmqpException("PublisherCallbackChannel is closed")); } @@ -1214,7 +1233,8 @@ else if (txEnds.contains(methodName)) { } } catch (InvocationTargetException ex) { - if (this.target == null || !this.target.isOpen()) { + Channel targetChannel = this.target; + if (targetChannel == null || !targetChannel.isOpen()) { // Basic re-connection logic... if (logger.isDebugEnabled()) { logger.debug("Detected closed channel on exception. Re-initializing: " + this.target); @@ -1262,12 +1282,14 @@ private void logicalClose(ChannelProxy proxy) throws IOException, TimeoutExcepti if (this.target == null) { return; } - if (this.target != null && !this.target.isOpen()) { + Channel targetChannel = this.target; + if (targetChannel != null && !targetChannel.isOpen()) { this.targetLock.lock(); try { - if (this.target != null && !this.target.isOpen()) { - if (this.target instanceof PublisherCallbackChannel) { - this.target.close(); // emit nacks if necessary + targetChannel = this.target; + if (targetChannel != null && !targetChannel.isOpen()) { + if (targetChannel instanceof PublisherCallbackChannel) { + targetChannel.close(); // emit nacks if necessary } if (!this.channelList.remove(proxy)) { releasePermitIfNecessary(); @@ -1283,6 +1305,7 @@ private void logicalClose(ChannelProxy proxy) throws IOException, TimeoutExcepti returnToCache(proxy); } + @SuppressWarnings("NullAway") // Dataflow analysis limitation private void returnToCache(ChannelProxy proxy) { if (CachingConnectionFactory.this.active && this.publisherConfirms @@ -1380,6 +1403,7 @@ private void setHighWaterMark() { } } + @SuppressWarnings("NullAway") // Dataflow analysis limitation private void physicalClose() throws IOException, TimeoutException { if (logger.isDebugEnabled()) { logger.debug("Closing cached Channel: " + this.target); @@ -1416,6 +1440,7 @@ private void physicalClose() throws IOException, TimeoutException { } } + @SuppressWarnings("NullAway") // Dataflow analysis limitation private void asyncClose() { ExecutorService executorService = getChannelsExecutor(); final Channel channel = CachedChannelInvocationHandler.this.target; @@ -1468,7 +1493,7 @@ private class ChannelCachingConnectionProxy implements ConnectionProxy { // NOSO private final ConcurrentMap channelsAwaitingAcks = new ConcurrentHashMap<>(); - private volatile Connection target; + private volatile @Nullable Connection target; ChannelCachingConnectionProxy(@Nullable Connection target) { this.target = target; @@ -1540,6 +1565,7 @@ private int countOpenIdleConnections() { return n; } + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void destroy() { if (CachingConnectionFactory.this.cacheMode == CacheMode.CHANNEL) { reset(CachingConnectionFactory.this.cachedChannelsNonTransactional, @@ -1565,17 +1591,24 @@ private void notifyCloseIfNecessary() { @Override public boolean isOpen() { - return this.target != null && this.target.isOpen(); + Connection targetToCheck = this.target; + return targetToCheck != null && targetToCheck.isOpen(); } @Override - public Connection getTargetConnection() { + public @Nullable Connection getTargetConnection() { return this.target; } @Override public com.rabbitmq.client.Connection getDelegate() { - return this.target.getDelegate(); + Connection targetConnection = this.target; + if (targetConnection != null) { + return targetConnection.getDelegate(); + } + else { + throw new IllegalStateException("Can't get delegate - no target connection."); + } } @Override diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ChannelListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ChannelListener.java index ad9f43e5ea..8d3451b620 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ChannelListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ChannelListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -39,7 +39,7 @@ public interface ChannelListener { /** * Called when the underlying RabbitMQ channel is closed for any * reason. - * @param signal the shut down signal. + * @param signal the shutdown signal. */ default void onShutDown(ShutdownSignalException signal) { } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ChannelProxy.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ChannelProxy.java index d72d096344..b2d382eaf6 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ChannelProxy.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ChannelProxy.java @@ -47,7 +47,7 @@ public interface ChannelProxy extends Channel, RawTargetAccess { /** * Return true if confirms are selected on this channel. - * @return true if confirms selected. + * @return true if {@code confirms} selected. * @since 2.1 */ default boolean isConfirmSelected() { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CompositeConnectionListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CompositeConnectionListener.java index e8d9746446..496205a976 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CompositeConnectionListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CompositeConnectionListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -21,6 +21,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import com.rabbitmq.client.ShutdownSignalException; +import org.jspecify.annotations.Nullable; /** * A composite listener that invokes its delegates in turn. @@ -35,7 +36,7 @@ public class CompositeConnectionListener implements ConnectionListener { private List delegates = new CopyOnWriteArrayList<>(); @Override - public void onCreate(Connection connection) { + public void onCreate(@Nullable Connection connection) { this.delegates.forEach(delegate -> delegate.onCreate(connection)); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/Connection.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/Connection.java index ca704414e3..9428fb0422 100755 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/Connection.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/Connection.java @@ -20,7 +20,6 @@ import com.rabbitmq.client.Channel; import org.springframework.amqp.AmqpException; -import org.springframework.lang.Nullable; /** * @author Dave Syer @@ -41,9 +40,8 @@ public interface Connection extends AutoCloseable { * Close this connection and all its channels * with the {@link com.rabbitmq.client.AMQP#REPLY_SUCCESS} close code * and message 'OK'. - * + *

* Waits for all the close operations to complete. - * * @throws AmqpException if an I/O problem is encountered */ @Override @@ -61,7 +59,6 @@ public interface Connection extends AutoCloseable { */ int getLocalPort(); - /** * Add a {@link BlockedListener}. * @param listener the listener to add @@ -84,9 +81,7 @@ public interface Connection extends AutoCloseable { * Return the underlying RabbitMQ connection. * @return the connection. */ - default @Nullable com.rabbitmq.client.Connection getDelegate() { - return null; - } + com.rabbitmq.client.Connection getDelegate(); /** * Close any channel associated with the current thread. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactory.java index d435f9b3dc..aaa0d0eb6b 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 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. @@ -16,8 +16,9 @@ package org.springframework.amqp.rabbit.connection; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.AmqpException; -import org.springframework.lang.Nullable; /** * An interface based ConnectionFactory for creating {@link com.rabbitmq.client.Connection Connections}. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryConfigurationUtils.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryConfigurationUtils.java index 32968e629b..e6e31f3e3e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryConfigurationUtils.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryConfigurationUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2025 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. @@ -18,6 +18,8 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + /** * Utility methods for configuring connection factories. * @@ -38,7 +40,7 @@ private ConnectionFactoryConfigurationUtils() { * @param clientConnectionProperties the properties. */ public static void updateClientConnectionProperties(AbstractConnectionFactory connectionFactory, - String clientConnectionProperties) { + @Nullable String clientConnectionProperties) { if (clientConnectionProperties != null) { String[] props = clientConnectionProperties.split(","); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryContextWrapper.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryContextWrapper.java index 57b5c2daad..7295c3165c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryContextWrapper.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryContextWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-2025 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. @@ -18,7 +18,8 @@ import java.util.concurrent.Callable; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.StringUtils; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryUtils.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryUtils.java index ca1b1f7201..d5aa3950aa 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryUtils.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionFactoryUtils.java @@ -20,9 +20,9 @@ import java.util.function.Consumer; import com.rabbitmq.client.Channel; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpIOException; -import org.springframework.lang.Nullable; import org.springframework.transaction.support.ResourceHolderSynchronization; import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; @@ -46,9 +46,9 @@ public final class ConnectionFactoryUtils { private static final boolean WEB_FLUX_PRESENT = ClassUtils.isPresent("org.springframework.web.reactive.function.client.WebClient", - ConnectionFactoryUtils.class.getClassLoader()); + ConnectionFactoryUtils.class.getClassLoader()); - private static final ThreadLocal COMPLETION_EXCEPTIONS = new ThreadLocal<>(); + private static final ThreadLocal<@Nullable AfterCompletionFailedException> COMPLETION_EXCEPTIONS = new ThreadLocal<>(); private static boolean captureAfterCompletionExceptions; @@ -63,9 +63,6 @@ private ConnectionFactoryUtils() { * @return whether the Channel is transactional */ public static boolean isChannelTransactional(Channel channel, ConnectionFactory connectionFactory) { - if (channel == null || connectionFactory == null) { - return false; - } RabbitResourceHolder resourceHolder = (RabbitResourceHolder) TransactionSynchronizationManager .getResource(connectionFactory); return (resourceHolder != null && resourceHolder.containsChannel(channel)); @@ -82,6 +79,7 @@ public static boolean isChannelTransactional(Channel channel, ConnectionFactory */ public static RabbitResourceHolder getTransactionalResourceHolder(final ConnectionFactory connectionFactory, final boolean synchedLocalTransactionAllowed) { + return getTransactionalResourceHolder(connectionFactory, synchedLocalTransactionAllowed, false); } @@ -108,8 +106,9 @@ public static RabbitResourceHolder getTransactionalResourceHolder(final Connecti * @param connectionFactory the RabbitMQ ConnectionFactory to bind for (used as TransactionSynchronizationManager * key) * @param resourceFactory the ResourceFactory to use for extracting or creating RabbitMQ resources - * @return the transactional Channel, or null if none found + * @return the transactional Channel */ + @SuppressWarnings("NullAway") // Dataflow analysis limitation private static RabbitResourceHolder doGetTransactionalResourceHolder(// NOSONAR complexity ConnectionFactory connectionFactory, ResourceFactory resourceFactory) { @@ -129,7 +128,7 @@ private static RabbitResourceHolder doGetTransactionalResourceHolder(// NOSONAR resourceHolderToUse = new RabbitResourceHolder(); } Connection connection = resourceFactory.getConnection(resourceHolderToUse); //NOSONAR - Channel channel = null; + Channel channel; try { /* * If we are in a listener container, first see if there's a channel registered @@ -160,7 +159,7 @@ private static RabbitResourceHolder doGetTransactionalResourceHolder(// NOSONAR if (!resourceHolderToUse.equals(resourceHolder) && TransactionSynchronizationManager.isSynchronizationActive()) { bindResourceToTransaction(resourceHolderToUse, connectionFactory, - resourceFactory.isSynchedLocalTransactionAllowed()); + resourceFactory.synchedLocalTransactionAllowed()); } return resourceHolderToUse; @@ -180,7 +179,7 @@ public static void releaseResources(@Nullable RabbitResourceHolder resourceHolde RabbitUtils.closeConnection(resourceHolder.getConnection()); } - public static RabbitResourceHolder bindResourceToTransaction(RabbitResourceHolder resourceHolder, + public static @Nullable RabbitResourceHolder bindResourceToTransaction(RabbitResourceHolder resourceHolder, ConnectionFactory connectionFactory, boolean synched) { if (TransactionSynchronizationManager.hasResource(connectionFactory) @@ -285,6 +284,7 @@ public interface ResourceFactory { * @param holder the RabbitResourceHolder * @return an appropriate Connection fetched from the holder, or null if none found */ + @Nullable Connection getConnection(RabbitResourceHolder holder); /** @@ -308,35 +308,20 @@ public interface ResourceFactory { * with the RabbitMQ transaction committing right after the main transaction. * @return whether to allow for synchronizing a local RabbitMQ transaction */ - boolean isSynchedLocalTransactionAllowed(); + boolean synchedLocalTransactionAllowed(); } - private static class RabbitResourceFactory implements ResourceFactory { - - private final ConnectionFactory connectionFactory; - - private final boolean synchedLocalTransactionAllowed; - - private final boolean publisherConnectionIfPossible; - - RabbitResourceFactory(ConnectionFactory connectionFactory, boolean synchedLocalTransactionAllowed, - boolean publisherConnectionIfPossible) { - - this.connectionFactory = connectionFactory; - this.synchedLocalTransactionAllowed = synchedLocalTransactionAllowed; - this.publisherConnectionIfPossible = publisherConnectionIfPossible; - } + private record RabbitResourceFactory(ConnectionFactory connectionFactory, boolean synchedLocalTransactionAllowed, + boolean publisherConnectionIfPossible) implements ResourceFactory { @Override - @Nullable - public Channel getChannel(RabbitResourceHolder holder) { + public @Nullable Channel getChannel(RabbitResourceHolder holder) { return holder.getChannel(); } @Override - @Nullable - public Connection getConnection(RabbitResourceHolder holder) { + public @Nullable Connection getConnection(RabbitResourceHolder holder) { return holder.getConnection(); } @@ -351,11 +336,6 @@ public Channel createChannel(Connection con) { return con.createChannel(this.synchedLocalTransactionAllowed); } - @Override - public boolean isSynchedLocalTransactionAllowed() { - return this.synchedLocalTransactionAllowed; - } - } /** diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionListener.java index 7154bbf99b..61ec8df30c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 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. @@ -17,6 +17,7 @@ package org.springframework.amqp.rabbit.connection; import com.rabbitmq.client.ShutdownSignalException; +import org.jspecify.annotations.Nullable; /** * A listener for connection creation and closing. @@ -32,7 +33,7 @@ public interface ConnectionListener { * Called when a new connection is established. * @param connection the connection. */ - void onCreate(Connection connection); + void onCreate(@Nullable Connection connection); /** * Called when a connection is closed. @@ -44,7 +45,7 @@ default void onClose(Connection connection) { /** * Called when a connection is force closed. - * @param signal the shut down signal. + * @param signal the shutdown signal. * @since 2.0 */ default void onShutDown(ShutdownSignalException signal) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionProxy.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionProxy.java index 29d4a2be82..b573fb9c57 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionProxy.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConnectionProxy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -16,6 +16,8 @@ package org.springframework.amqp.rabbit.connection; +import org.jspecify.annotations.Nullable; + /** * Subinterface of {@link Connection} to be implemented by * Connection proxies. Allows access to the underlying target Connection @@ -28,7 +30,9 @@ public interface ConnectionProxy extends Connection { /** * Return the target Channel of this proxy. *

This will typically be the native provider Connection - * @return the underlying Connection (never null) + * @return the underlying Connection (if any) */ + @Nullable Connection getTargetConnection(); + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConsumerChannelRegistry.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConsumerChannelRegistry.java index 4ae774016e..51ee5febbd 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConsumerChannelRegistry.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ConsumerChannelRegistry.java @@ -19,8 +19,7 @@ import com.rabbitmq.client.Channel; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Consumers register their primary channels with this class. This is used @@ -38,8 +37,7 @@ public final class ConsumerChannelRegistry { private static final Log logger = LogFactory.getLog(ConsumerChannelRegistry.class); // NOSONAR - lower case - private static final ThreadLocal consumerChannel // NOSONAR - lower case - = new ThreadLocal<>(); + private static final ThreadLocal<@Nullable ChannelHolder> CONSUMER_CHANNEL = new ThreadLocal<>(); private ConsumerChannelRegistry() { } @@ -58,9 +56,9 @@ private ConsumerChannelRegistry() { public static void registerConsumerChannel(Channel channel, ConnectionFactory connectionFactory) { if (logger.isDebugEnabled()) { logger.debug("Registering consumer channel" + channel + " from factory " + - connectionFactory); + connectionFactory); } - consumerChannel.set(new ChannelHolder(channel, connectionFactory)); + CONSUMER_CHANNEL.set(new ChannelHolder(channel, connectionFactory)); } /** @@ -69,9 +67,9 @@ public static void registerConsumerChannel(Channel channel, ConnectionFactory co */ public static void unRegisterConsumerChannel() { if (logger.isDebugEnabled()) { - logger.debug("Unregistering consumer channel" + consumerChannel.get()); + logger.debug("Unregistering consumer channel" + CONSUMER_CHANNEL.get()); } - consumerChannel.remove(); + CONSUMER_CHANNEL.remove(); } /** @@ -80,11 +78,10 @@ public static void unRegisterConsumerChannel() { * * @return The channel. */ - @Nullable - public static Channel getConsumerChannel() { - ChannelHolder channelHolder = consumerChannel.get(); + public static @Nullable Channel getConsumerChannel() { + ChannelHolder channelHolder = CONSUMER_CHANNEL.get(); return channelHolder != null - ? channelHolder.getChannel() + ? channelHolder.channel() : null; } @@ -95,33 +92,17 @@ public static Channel getConsumerChannel() { * @param connectionFactory The connection factory. * @return The channel. */ - @Nullable - public static Channel getConsumerChannel(ConnectionFactory connectionFactory) { - ChannelHolder channelHolder = consumerChannel.get(); + public static @Nullable Channel getConsumerChannel(ConnectionFactory connectionFactory) { + ChannelHolder channelHolder = CONSUMER_CHANNEL.get(); Channel channel = null; - if (channelHolder != null && channelHolder.getConnectionFactory().equals(connectionFactory)) { - channel = channelHolder.getChannel(); + if (channelHolder != null && channelHolder.connectionFactory().equals(connectionFactory)) { + channel = channelHolder.channel(); } return channel; } - private static final class ChannelHolder { - - private final Channel channel; - - private final ConnectionFactory connectionFactory; - - ChannelHolder(Channel channel, ConnectionFactory connectionFactory) { - this.channel = channel; - this.connectionFactory = connectionFactory; - } + private record ChannelHolder(Channel channel, ConnectionFactory connectionFactory) { - private Channel getChannel() { - return this.channel; - } - - private ConnectionFactory getConnectionFactory() { - return this.connectionFactory; - } } + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java index 476d21646c..14495ab464 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 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. @@ -19,9 +19,10 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Correlation; import org.springframework.amqp.core.ReturnedMessage; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -44,10 +45,10 @@ public class CorrelationData implements Correlation { private volatile String id; - private volatile ReturnedMessage returnedMessage; + private volatile @Nullable ReturnedMessage returnedMessage; /** - * Construct an instance with a null Id. + * Construct an instance with a null {@code Id}. * @since 1.6.7 */ public CorrelationData() { @@ -76,7 +77,6 @@ public String getId() { * Set the correlation id. Generally, the correlation id shouldn't be changed. * One use case, however, is when it needs to be set in a * {@link org.springframework.amqp.core.MessagePostProcessor}. - * * @param id the id. * @since 1.6 */ @@ -100,8 +100,7 @@ public CompletableFuture getFuture() { * @return the {@link ReturnedMessage}. * @since 2.3.3 */ - @Nullable - public ReturnedMessage getReturned() { + public @Nullable ReturnedMessage getReturned() { return this.returnedMessage; } @@ -122,35 +121,14 @@ public String toString() { /** * Represents a publisher confirmation. When the ack field is * true, the publish was successful; otherwise failed with a possible - * reason (may be null, meaning unknown). + * reason (maybe null, meaning unknown). + * + * @param ack true to confirm + * @param reason the reason for nack * * @since 2.1 */ - public static class Confirm { - - private final boolean ack; - - private final String reason; - - public Confirm(boolean ack, @Nullable String reason) { - this.ack = ack; - this.reason = reason; - } - - public boolean isAck() { - return this.ack; - } - - public String getReason() { - return this.reason; - } - - @Override - public String toString() { - return "Confirm [ack=" + this.ack - + (this.reason != null ? ", reason=" + this.reason : "") - + "]"; - } + public record Confirm(boolean ack, @Nullable String reason) { } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/FactoryFinder.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/FactoryFinder.java index 8e26ad9524..0d75ab329b 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/FactoryFinder.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/FactoryFinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2025 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. @@ -16,6 +16,8 @@ package org.springframework.amqp.rabbit.connection; +import org.jspecify.annotations.Nullable; + /** * Callback to determine the connection factory using the provided information. * @@ -32,6 +34,7 @@ public interface FactoryFinder { * @param nodeUri the node URI. * @return the factory. */ - ConnectionFactory locate(String queueName, String node, String nodeUri); + @Nullable + ConnectionFactory locate(@Nullable String queueName, String node, String nodeUri); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java index c27986322b..a4efd22a2f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/LocalizedQueueConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 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. @@ -20,6 +20,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -28,13 +29,13 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpException; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.beans.factory.DisposableBean; import org.springframework.context.SmartLifecycle; import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -78,15 +79,15 @@ public class LocalizedQueueConnectionFactory implements ConnectionFactory, Routi private final boolean useSSL; - private final Resource sslPropertiesLocation; + private final @Nullable Resource sslPropertiesLocation; - private final String keyStore; + private final @Nullable String keyStore; - private final String trustStore; + private final @Nullable String trustStore; - private final String keyStorePassPhrase; + private final @Nullable String keyStorePassPhrase; - private final String trustStorePassPhrase; + private final @Nullable String trustStorePassPhrase; private final AtomicBoolean running = new AtomicBoolean(); @@ -95,11 +96,11 @@ public class LocalizedQueueConnectionFactory implements ConnectionFactory, Routi /** * @param defaultConnectionFactory the fallback connection factory to use if the queue * can't be located. - * @param nodeToAddress a Map of node to address: (rabbit@server1 : server1:5672) - * @param adminUris the rabbitmq admin addresses (https://host:port, ...) must be the + * @param nodeToAddress a Map of node to address: {@code rabbit@server1 : server1:5672} + * @param adminUris the rabbitmq admin addresses ({@code https://host:port, ...}) must be the * same length as addresses. * @param vhost the virtual host. - * @param username the user name. + * @param username the username. * @param password the password. * @param useSSL use SSL. * @param sslPropertiesLocation the SSL properties location. @@ -114,11 +115,11 @@ public LocalizedQueueConnectionFactory(ConnectionFactory defaultConnectionFactor /** * @param defaultConnectionFactory the fallback connection factory to use if the queue can't be located. - * @param nodeToAddress a Map of node to address: (rabbit@server1 : server1:5672) - * @param adminUris the rabbitmq admin addresses (https://host:port, ...) must be the same length + * @param nodeToAddress a Map of node to address: {@code rabbit@server1 : server1:5672} + * @param adminUris the rabbitmq admin addresses ({@code https://host:port, ...}) must be the same length * as addresses. * @param vhost the virtual host. - * @param username the user name. + * @param username the username. * @param password the password. * @param useSSL use SSL. * @param keyStore the key store resource (e.g. "file:/foo/keystore"). @@ -139,11 +140,11 @@ public LocalizedQueueConnectionFactory(ConnectionFactory defaultConnectionFactor * @param defaultConnectionFactory the fallback connection factory to use if the queue * can't be located. * @param addresses the rabbitmq server addresses (host:port, ...). - * @param adminUris the rabbitmq admin addresses (https://host:port, ...) + * @param adminUris the rabbitmq admin addresses ({@code https://host:port, ...}) * @param nodes the rabbitmq nodes corresponding to addresses (rabbit@server1, ...) * must be the same length as addresses. * @param vhost the virtual host. - * @param username the user name. + * @param username the username. * @param password the password. * @param useSSL use SSL. * @param sslPropertiesLocation the SSL properties location. @@ -159,11 +160,11 @@ public LocalizedQueueConnectionFactory(ConnectionFactory defaultConnectionFactor /** * @param defaultConnectionFactory the fallback connection factory to use if the queue can't be located. * @param addresses the rabbitmq server addresses (host:port, ...). - * @param adminUris the rabbitmq admin addresses (https://host:port, ...). + * @param adminUris the rabbitmq admin addresses ({@code https://host:port, ...}). * @param nodes the rabbitmq nodes corresponding to addresses (rabbit@server1, ...) must be the same length * as addresses. * @param vhost the virtual host. - * @param username the user name. + * @param username the username. * @param password the password. * @param useSSL use SSL. * @param keyStore the key store resource (e.g. "file:/foo/keystore"). @@ -205,8 +206,8 @@ private static Map nodesAddressesToMap(String[] nodes, String[] Assert.isTrue(addresses.length == nodes.length, "'addresses' and 'nodes' properties must have equal length"); return IntStream.range(0, addresses.length) - .mapToObj(i -> new SimpleImmutableEntry<>(nodes[i], addresses[i])) - .collect(Collectors.toMap(SimpleImmutableEntry::getKey, SimpleImmutableEntry::getValue)); + .mapToObj(i -> new SimpleImmutableEntry<>(nodes[i], addresses[i])) + .collect(Collectors.toMap(SimpleImmutableEntry::getKey, SimpleImmutableEntry::getValue)); } /** @@ -225,7 +226,7 @@ public Connection createConnection() throws AmqpException { } @Override - public String getHost() { + public @Nullable String getHost() { return this.defaultConnectionFactory.getHost(); } @@ -287,16 +288,10 @@ public ConnectionFactory getTargetConnectionFactory(Object key) { Assert.isTrue(!queue.contains(","), () -> "Cannot use LocalizedQueueConnectionFactory with more than one queue: " + key); ConnectionFactory connectionFactory = determineConnectionFactory(queue); - if (connectionFactory == null) { - return this.defaultConnectionFactory; - } - else { - return connectionFactory; - } + return Objects.requireNonNullElse(connectionFactory, this.defaultConnectionFactory); } - @Nullable - private ConnectionFactory determineConnectionFactory(String queue) { + private @Nullable ConnectionFactory determineConnectionFactory(String queue) { ConnectionFactory cf = this.nodeLocator.locate(this.adminUris, this.nodeToAddress, this.vhost, this.username, this.password, queue, this::nodeConnectionFactory); if (cf == null) { @@ -305,7 +300,7 @@ private ConnectionFactory determineConnectionFactory(String queue) { return cf; } - private ConnectionFactory nodeConnectionFactory(String queue, String node, String address) { + private ConnectionFactory nodeConnectionFactory(@Nullable String queue, String node, String address) { this.lock.lock(); try { if (this.logger.isInfoEnabled()) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/NodeLocator.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/NodeLocator.java index aa5dfb2331..4b4846f6ce 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/NodeLocator.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/NodeLocator.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2022-2025 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. @@ -21,16 +21,18 @@ import java.util.Map; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpException; import org.springframework.core.log.LogAccessor; -import org.springframework.lang.Nullable; /** * Used to obtain a connection factory for the queue leader. * @param the client type. * * @author Gary Russell + * @author Artem Bilan + * * @since 2.4.8 */ public interface NodeLocator { @@ -42,20 +44,20 @@ public interface NodeLocator { * @param adminUris an array of admin URIs. * @param nodeToAddress a map of node names to node addresses (AMQP). * @param vhost the vhost. - * @param username the user name. + * @param username the username. * @param password the password. * @param queue the queue name. * @param factoryFunction an internal function to find or create the factory. * @return a connection factory, if the leader node was found; null otherwise. */ - @Nullable - default ConnectionFactory locate(String[] adminUris, Map nodeToAddress, String vhost, - String username, String password, String queue, FactoryFinder factoryFunction) { + default @Nullable ConnectionFactory locate(String[] adminUris, Map nodeToAddress, + String vhost, String username, String password, String queue, + FactoryFinder factoryFunction) { T client = createClient(username, password); - for (int i = 0; i < adminUris.length; i++) { - String adminUri = adminUris[i]; + for (String uris : adminUris) { + String adminUri = uris; if (!adminUri.endsWith("/api/")) { adminUri += "/api/"; } @@ -92,7 +94,7 @@ default ConnectionFactory locate(String[] adminUris, Map nodeToA /** * Create a client for subsequent use. - * @param userName the user name. + * @param userName the username. * @param password the password. * @return the client. */ diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PendingConfirm.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PendingConfirm.java index 5aa67760c9..2d65393e33 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PendingConfirm.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PendingConfirm.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -19,7 +19,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Instances of this object track pending publisher confirms. @@ -35,14 +35,13 @@ public class PendingConfirm { static final long RETURN_CALLBACK_TIMEOUT = 60; - @Nullable - private final CorrelationData correlationData; + private final @Nullable CorrelationData correlationData; private final long timestamp; private final CountDownLatch latch = new CountDownLatch(1); - private String cause; + private @Nullable String cause; private boolean returned; @@ -85,8 +84,7 @@ public void setCause(String cause) { * @return the cause. * @since 1.4 */ - @Nullable - public String getCause() { + public @Nullable String getCause() { return this.cause; } @@ -129,7 +127,8 @@ public void countDown() { @Override public String toString() { - return "PendingConfirm [correlationData=" + this.correlationData + (this.cause == null ? "" : " cause=" + this.cause) + "]"; + return "PendingConfirm [correlationData=" + this.correlationData + + (this.cause == null ? "" : " cause=" + this.cause) + "]"; } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java index d2ee951747..b2aeac66aa 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactory.java @@ -36,25 +36,27 @@ import org.apache.commons.pool2.PooledObjectFactory; import org.apache.commons.pool2.impl.DefaultPooledObject; import org.apache.commons.pool2.impl.GenericObjectPool; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpException; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.support.NameMatchMethodPointcutAdvisor; import org.springframework.context.SmartLifecycle; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** * A very simple connection factory that caches channels using Apache Pool2 * {@link GenericObjectPool}s (one for transactional and one for non-transactional - * channels). The pools have default configuration but they can be configured using + * channels). The pools have default configuration, but they can be configured using * a callback. * * @author Gary Russell * @author Leonardo Ferreira * @author Christian Tzolov * @author Ngoc Nhan + * @author Artem Bilan + * * @since 2.3 * */ @@ -65,11 +67,12 @@ public class PooledChannelConnectionFactory extends AbstractConnectionFactory private final Lock lock = new ReentrantLock(); - private volatile ConnectionWrapper connection; + private volatile @Nullable ConnectionWrapper connection; private boolean simplePublisherConfirms; - private BiConsumer, Boolean> poolConfigurer = (pool, tx) -> { }; + private BiConsumer, Boolean> poolConfigurer = (pool, tx) -> { + }; private boolean defaultPublisherFactory = true; @@ -86,6 +89,7 @@ public PooledChannelConnectionFactory(ConnectionFactory rabbitConnectionFactory) * @param rabbitConnectionFactory the rabbitmq connection factory. * @param isPublisher true if we are creating a publisher connection factory. */ + @SuppressWarnings("this-escape") private PooledChannelConnectionFactory(ConnectionFactory rabbitConnectionFactory, boolean isPublisher) { super(rabbitConnectionFactory); if (!isPublisher) { @@ -107,6 +111,7 @@ public void setPublisherConnectionFactory(@Nullable AbstractConnectionFactory pu * called with the transactional pool. * @param poolConfigurer the configurer. */ + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void setPoolConfigurer(BiConsumer, Boolean> poolConfigurer) { Assert.notNull(poolConfigurer, "'poolConfigurer' cannot be null"); this.poolConfigurer = poolConfigurer; // NOSONAR - sync inconsistency @@ -124,11 +129,12 @@ public boolean isSimplePublisherConfirms() { * Enable simple publisher confirms. * @param simplePublisherConfirms true to enable. */ + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void setSimplePublisherConfirms(boolean simplePublisherConfirms) { this.simplePublisherConfirms = simplePublisherConfirms; if (this.defaultPublisherFactory) { ((PooledChannelConnectionFactory) getPublisherConnectionFactory()) - .setSimplePublisherConfirms(simplePublisherConfirms); // NOSONAR + .setSimplePublisherConfirms(simplePublisherConfirms); // NOSONAR } } @@ -136,8 +142,9 @@ public void setSimplePublisherConfirms(boolean simplePublisherConfirms) { public void addConnectionListener(ConnectionListener listener) { super.addConnectionListener(listener); // handles publishing sub-factory // If the connection is already alive we assume that the new listener wants to be notified - if (this.connection != null && this.connection.isOpen()) { - listener.onCreate(this.connection); + ConnectionWrapper connectionWrapper = this.connection; + if (connectionWrapper != null && connectionWrapper.isOpen()) { + listener.onCreate(connectionWrapper); } } @@ -164,19 +171,24 @@ public boolean isRunning() { @Override public Connection createConnection() throws AmqpException { - this.lock.lock(); - try { - if (this.connection == null || !this.connection.isOpen()) { - Connection bareConnection = createBareConnection(); // NOSONAR - see destroy() - this.connection = new ConnectionWrapper(bareConnection.getDelegate(), getCloseTimeout(), // NOSONAR - this.simplePublisherConfirms, this.poolConfigurer, getChannelListener()); // NOSONAR - getConnectionListener().onCreate(this.connection); + ConnectionWrapper connectionWrapper = this.connection; + if (connectionWrapper == null || !connectionWrapper.isOpen()) { + this.lock.lock(); + try { + connectionWrapper = this.connection; + if (connectionWrapper == null || !connectionWrapper.isOpen()) { + Connection bareConnection = createBareConnection(); + connectionWrapper = new ConnectionWrapper(bareConnection.getDelegate(), getCloseTimeout(), + this.simplePublisherConfirms, this.poolConfigurer, getChannelListener()); + this.connection = connectionWrapper; + getConnectionListener().onCreate(this.connection); + } + } + finally { + this.lock.unlock(); } - return this.connection; - } - finally { - this.lock.unlock(); } + return connectionWrapper; } /** @@ -195,9 +207,10 @@ public void destroy() { this.lock.lock(); try { super.destroy(); - if (this.connection != null) { - this.connection.forceClose(); - getConnectionListener().onClose(this.connection); + ConnectionWrapper connectionWrapper = this.connection; + if (connectionWrapper != null) { + connectionWrapper.forceClose(); + getConnectionListener().onClose(connectionWrapper); this.connection = null; } } @@ -248,6 +261,7 @@ public Channel createChannel(boolean transactional) { } } + @SuppressWarnings("NullAway") // Dataflow analysis limitation private Channel createProxy(Channel channel, boolean transacted) { ProxyFactory pf = new ProxyFactory(channel); AtomicReference proxy = new AtomicReference<>(); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java index 3bc95d412d..9684c8505f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/PublisherCallbackChannelImpl.java @@ -71,6 +71,7 @@ import com.rabbitmq.client.impl.recovery.AutorecoveringChannel; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; @@ -78,7 +79,6 @@ import org.springframework.amqp.rabbit.connection.CorrelationData.Confirm; import org.springframework.amqp.rabbit.support.DefaultMessagePropertiesConverter; import org.springframework.amqp.rabbit.support.MessagePropertiesConverter; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -116,7 +116,7 @@ public class PublisherCallbackChannelImpl private final ExecutorService executor; - private volatile java.util.function.Consumer afterAckCallback; + private volatile java.util.function.@Nullable Consumer afterAckCallback; /** * Create a {@link PublisherCallbackChannelImpl} instance based on the provided @@ -124,6 +124,7 @@ public class PublisherCallbackChannelImpl * @param delegate the delegate channel. * @param executor the executor. */ + @SuppressWarnings("this-escape") public PublisherCallbackChannelImpl(Channel delegate, ExecutorService executor) { Assert.notNull(executor, "'executor' must not be null"); this.delegate = delegate; @@ -135,7 +136,7 @@ public PublisherCallbackChannelImpl(Channel delegate, ExecutorService executor) public void setAfterAckCallback(java.util.function.Consumer callback) { this.lock.lock(); try { - if (getPendingConfirmsCount() == 0 && callback != null) { + if (getPendingConfirmsCount() == 0) { callback.accept(this); } else { @@ -147,10 +148,6 @@ public void setAfterAckCallback(java.util.function.Consumer callback) { } } -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// BEGIN PURE DELEGATE METHODS -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - @Override public void addShutdownListener(ShutdownListener listener) { this.delegate.addShutdownListener(listener); @@ -816,10 +813,6 @@ public Channel getDelegate() { return this.delegate; } -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// END PURE DELEGATE METHODS -////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - @Override public void close() throws IOException, TimeoutException { if (this.logger.isDebugEnabled()) { @@ -848,7 +841,7 @@ private void generateNacksForPendingAcks(String cause) { for (Entry confirmEntry : entry.getValue().entrySet()) { confirmEntry.getValue().setCause(cause); if (this.logger.isDebugEnabled()) { - this.logger.debug(this.toString() + " PC:Nack:(close):" + confirmEntry.getKey()); + this.logger.debug(this + " PC:Nack:(close):" + confirmEntry.getKey()); } processAck(confirmEntry.getKey(), false, false, false); } @@ -952,7 +945,7 @@ public Collection expire(Listener listener, long cutoffTime) { @Override public void handleAck(long seq, boolean multiple) { if (this.logger.isDebugEnabled()) { - this.logger.debug(this.toString() + " PC:Ack:" + seq + ":" + multiple); + this.logger.debug(this + " PC:Ack:" + seq + ":" + multiple); } processAck(seq, true, multiple, true); } @@ -960,7 +953,7 @@ public void handleAck(long seq, boolean multiple) { @Override public void handleNack(long seq, boolean multiple) { if (this.logger.isDebugEnabled()) { - this.logger.debug(this.toString() + " PC:Nack:" + seq + ":" + multiple); + this.logger.debug(this + " PC:Nack:" + seq + ":" + multiple); } processAck(seq, false, multiple, true); } @@ -1018,7 +1011,7 @@ private void doProcessAck(long seq, boolean ack, boolean multiple, boolean remov private void processMultipleAck(long seq, boolean ack) { /* - * Piggy-backed ack - extract all Listeners for this and earlier + * Piggybacked ack - extract all Listeners for this and earlier * sequences. Then, for each Listener, handle each of it's acks. * Finally, remove the sequences from listenerForSeq. */ @@ -1125,7 +1118,7 @@ public void addPendingConfirm(Listener listener, long seq, PendingConfirm pendin public void handle(Return returned) { if (this.logger.isDebugEnabled()) { - this.logger.debug("Return " + this.toString()); + this.logger.debug("Return " + this); } PendingConfirm confirm = findConfirm(returned); Listener listener = findListener(returned.getProperties()); @@ -1138,26 +1131,23 @@ public void handle(Return returned) { if (confirm != null) { confirm.setReturned(true); } - Listener listenerToInvoke = listener; - PendingConfirm toCountDown = confirm; this.executor.execute(() -> { try { - listenerToInvoke.handleReturn(returned); + listener.handleReturn(returned); } catch (Exception e) { this.logger.error("Exception delivering returned message ", e); } finally { - if (toCountDown != null) { - toCountDown.countDown(); + if (confirm != null) { + confirm.countDown(); } } }); } } - @Nullable - private PendingConfirm findConfirm(Return returned) { + private @Nullable PendingConfirm findConfirm(Return returned) { LongString returnCorrelation = (LongString) returned.getProperties().getHeaders() .get(RETURNED_MESSAGE_CORRELATION_KEY); PendingConfirm confirm = null; @@ -1177,8 +1167,7 @@ private PendingConfirm findConfirm(Return returned) { return confirm; } - @Nullable - private Listener findListener(AMQP.BasicProperties properties) { + private @Nullable Listener findListener(AMQP.BasicProperties properties) { Listener listener = null; Object returnListenerHeader = properties.getHeaders().get(RETURN_LISTENER_CORRELATION_KEY); String uuidObject = null; @@ -1198,7 +1187,8 @@ private Listener findListener(AMQP.BasicProperties properties) { @Override public void shutdownCompleted(ShutdownSignalException cause) { - shutdownCompleted(cause.getMessage()); + String causeMessage = cause.getMessage(); + shutdownCompleted(causeMessage != null ? causeMessage : "Normal"); } // Object @@ -1216,7 +1206,7 @@ public boolean equals(Object obj) { @Override public String toString() { - return "PublisherCallbackChannelImpl: " + this.delegate.toString(); + return "PublisherCallbackChannelImpl: " + this.delegate; } public static PublisherCallbackChannelFactory factory() { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java index 768b789e86..5ccc9e8242 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java @@ -20,12 +20,12 @@ import io.micrometer.observation.ObservationRegistry; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.ApplicationContext; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -39,6 +39,7 @@ public abstract class RabbitAccessor implements InitializingBean { /** Logger available to subclasses. */ protected final Log logger = LogFactory.getLog(getClass()); // NOSONAR + @SuppressWarnings("NullAway.Init") private volatile ConnectionFactory connectionFactory; private volatile boolean transactional; @@ -94,8 +95,7 @@ protected Connection createConnection() { * @param holder the RabbitResourceHolder * @return an appropriate Connection fetched from the holder, or null if none found */ - @Nullable - protected Connection getConnection(RabbitResourceHolder holder) { + protected @Nullable Connection getConnection(RabbitResourceHolder holder) { return holder.getConnection(); } @@ -105,8 +105,7 @@ protected Connection getConnection(RabbitResourceHolder holder) { * @param holder the RabbitResourceHolder * @return an appropriate Channel fetched from the holder, or null if none found */ - @Nullable - protected Channel getChannel(RabbitResourceHolder holder) { + protected @Nullable Channel getChannel(RabbitResourceHolder holder) { return holder.getChannel(); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitConnectionFactoryBean.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitConnectionFactoryBean.java index 50ead3fe44..9b276b8b50 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitConnectionFactoryBean.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitConnectionFactoryBean.java @@ -48,13 +48,13 @@ import com.rabbitmq.client.impl.CredentialsProvider; import com.rabbitmq.client.impl.CredentialsRefreshService; import com.rabbitmq.client.impl.nio.NioParams; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.beans.factory.config.AbstractFactoryBean; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -130,29 +130,29 @@ public class RabbitConnectionFactoryBean extends AbstractFactoryBean never be set to true in production * @param skipServerCertificateValidation Flag to override Server side certificate checks; @@ -235,15 +235,15 @@ protected String getSslAlgorithm() { * set property in this bean. * @param sslPropertiesLocation the Resource to the ssl properties */ - public void setSslPropertiesLocation(Resource sslPropertiesLocation) { + public void setSslPropertiesLocation(@Nullable Resource sslPropertiesLocation) { this.sslPropertiesLocation = sslPropertiesLocation; } /** - * @return the properties location. + * @return the properties file location. * @since 1.4.4 */ - protected Resource getSslPropertiesLocation() { + protected @Nullable Resource getSslPropertiesLocation() { return this.sslPropertiesLocation; } @@ -263,11 +263,11 @@ protected String getKeyStore() { * @param keyStore the keystore resource. * @since 1.5 */ - public void setKeyStore(String keyStore) { + public void setKeyStore(@Nullable String keyStore) { this.keyStore = keyStore; } - protected Resource getKeyStoreResource() { + protected @Nullable Resource getKeyStoreResource() { return this.keyStoreResource; } @@ -296,11 +296,11 @@ protected String getTrustStore() { * @param trustStore the truststore resource. * @since 1.5 */ - public void setTrustStore(String trustStore) { + public void setTrustStore(@Nullable String trustStore) { this.trustStore = trustStore; } - protected Resource getTrustStoreResource() { + protected @Nullable Resource getTrustStoreResource() { return this.trustStoreResource; } @@ -317,7 +317,7 @@ public void setTrustStoreResource(Resource trustStoreResource) { * @return the key store pass phrase. * @since 1.5 */ - protected String getKeyStorePassphrase() { + protected @Nullable String getKeyStorePassphrase() { return this.keyStorePassphrase == null ? this.sslProperties.getProperty(KEY_STORE_PASS_PHRASE) : this.keyStorePassphrase; } @@ -328,7 +328,7 @@ protected String getKeyStorePassphrase() { * @param keyStorePassphrase the key store pass phrase. * @since 1.5 */ - public void setKeyStorePassphrase(String keyStorePassphrase) { + public void setKeyStorePassphrase(@Nullable String keyStorePassphrase) { this.keyStorePassphrase = keyStorePassphrase; } @@ -336,7 +336,7 @@ public void setKeyStorePassphrase(String keyStorePassphrase) { * @return the trust store pass phrase. * @since 1.5 */ - protected String getTrustStorePassphrase() { + protected @Nullable String getTrustStorePassphrase() { return this.trustStorePassphrase == null ? this.sslProperties.getProperty(TRUST_STORE_PASS_PHRASE) : this.trustStorePassphrase; } @@ -347,7 +347,7 @@ protected String getTrustStorePassphrase() { * @param trustStorePassphrase the trust store pass phrase. * @since 1.5 */ - public void setTrustStorePassphrase(String trustStorePassphrase) { + public void setTrustStorePassphrase(@Nullable String trustStorePassphrase) { this.trustStorePassphrase = trustStorePassphrase; } @@ -375,7 +375,7 @@ protected String getKeyStoreType() { * @since 1.6.2 * @see java.security.KeyStore#getInstance(String) */ - public void setKeyStoreType(String keyStoreType) { + public void setKeyStoreType(@Nullable String keyStoreType) { this.keyStoreType = keyStoreType; } @@ -403,11 +403,11 @@ protected String getTrustStoreType() { * @since 1.6.2 * @see java.security.KeyStore#getInstance(String) */ - public void setTrustStoreType(String trustStoreType) { + public void setTrustStoreType(@Nullable String trustStoreType) { this.trustStoreType = trustStoreType; } - protected SecureRandom getSecureRandom() { + protected @Nullable SecureRandom getSecureRandom() { return this.secureRandom; } @@ -439,7 +439,7 @@ public void setPort(int port) { } /** - * @param username the user name. + * @param username the username. * @see com.rabbitmq.client.ConnectionFactory#setUsername(java.lang.String) */ public void setUsername(String username) { @@ -596,7 +596,7 @@ public void setExceptionHandler(ExceptionHandler exceptionHandler) { } /** - * Whether or not the factory should be configured to use Java NIO. + * Whether the factory should be configured to use Java NIO. * @param useNio true to use Java NIO, false to use blocking IO * @see com.rabbitmq.client.ConnectionFactory#useNio() */ @@ -811,8 +811,8 @@ private void setupBasicSSL() throws NoSuchAlgorithmException, KeyManagementExcep } } - @Nullable - protected KeyManager[] configureKeyManagers() throws KeyStoreException, IOException, NoSuchAlgorithmException, + protected KeyManager @Nullable [] configureKeyManagers() + throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException { String keyStoreName = getKeyStore(); String keyStorePassword = getKeyStorePassphrase(); @@ -836,8 +836,7 @@ protected KeyManager[] configureKeyManagers() throws KeyStoreException, IOExcept return keyManagers; } - @Nullable - protected TrustManager[] configureTrustManagers() + protected TrustManager @Nullable [] configureTrustManagers() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException { String trustStoreName = getTrustStore(); String trustStorePassword = getTrustStorePassphrase(); @@ -872,7 +871,6 @@ protected SSLContext createSSLContext() throws NoSuchAlgorithmException { return SSLContext.getInstance(this.sslAlgorithm); } - private void useDefaultTrustStoreMechanism() throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException { SSLContext sslContext = SSLContext.getInstance(this.sslAlgorithm); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitResourceHolder.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitResourceHolder.java index 925b3bf39f..9824477081 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitResourceHolder.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitResourceHolder.java @@ -25,10 +25,10 @@ import com.rabbitmq.client.Channel; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpException; import org.springframework.amqp.AmqpIOException; -import org.springframework.lang.Nullable; import org.springframework.transaction.support.ResourceHolderSupport; import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; @@ -118,11 +118,8 @@ public final void addChannel(Channel channel, @Nullable Connection connection) { if (!this.channels.contains(channel)) { this.channels.add(channel); if (connection != null) { - List channelsForConnection = this.channelsPerConnection.get(connection); - if (channelsForConnection == null) { - channelsForConnection = new LinkedList<>(); - this.channelsPerConnection.put(connection, channelsForConnection); - } + List channelsForConnection = + this.channelsPerConnection.computeIfAbsent(connection, k -> new LinkedList<>()); channelsForConnection.add(channel); } } @@ -132,13 +129,11 @@ public boolean containsChannel(Channel channel) { return this.channels.contains(channel); } - @Nullable - public Connection getConnection() { + public @Nullable Connection getConnection() { return (!this.connections.isEmpty() ? this.connections.get(0) : null); } - @Nullable - public Channel getChannel() { + public @Nullable Channel getChannel() { return (!this.channels.isEmpty() ? this.channels.get(0) : null); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java index 9a1fec7707..13502ab92e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitUtils.java @@ -31,10 +31,10 @@ import com.rabbitmq.client.impl.recovery.AutorecoveringChannel; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpIOException; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -82,12 +82,12 @@ public abstract class RabbitUtils { private static final Log LOGGER = LogFactory.getLog(RabbitUtils.class); - private static final ThreadLocal physicalCloseRequired = new ThreadLocal<>(); // NOSONAR - lower case + private static final ThreadLocal<@Nullable Boolean> physicalCloseRequired = new ThreadLocal<>(); // NOSONAR - lower case /** * Close the given RabbitMQ Connection and ignore any thrown exception. This is useful for typical * finally blocks in manual RabbitMQ code. - * @param connection the RabbitMQ Connection to close (may be null) + * @param connection the RabbitMQ Connection to close (maybe null) */ public static void closeConnection(@Nullable Connection connection) { if (connection != null) { @@ -106,7 +106,7 @@ public static void closeConnection(@Nullable Connection connection) { /** * Close the given RabbitMQ Channel and ignore any thrown exception. This is useful for typical finally * blocks in manual RabbitMQ code. - * @param channel the RabbitMQ Channel to close (may be null) + * @param channel the RabbitMQ Channel to close (maybe null) */ public static void closeChannel(@Nullable Channel channel) { if (channel != null) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateNodeLocator.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateNodeLocator.java index dacefb649f..5f66d50d75 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateNodeLocator.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RestTemplateNodeLocator.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 the original author or authors. + * Copyright 2022-2025 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. @@ -26,6 +26,7 @@ import org.apache.hc.client5.http.impl.auth.BasicScheme; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.core5.http.HttpHost; +import org.jspecify.annotations.Nullable; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpMethod; @@ -33,7 +34,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.http.client.support.BasicAuthenticationInterceptor; -import org.springframework.lang.Nullable; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriUtils; @@ -66,8 +66,8 @@ public RestTemplate createClient(String userName, String password) { } @Override - @Nullable - public Map restCall(RestTemplate client, String baseUri, String vhost, String queue) { + public @Nullable Map restCall(RestTemplate client, String baseUri, String vhost, String queue) { + URI theBaseUri = URI.create(baseUri); if (!this.authSchemeIsSetToCache.getAndSet(true)) { this.authCache.put(HttpHost.create(theBaseUri), new BasicScheme()); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RoutingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RoutingConnectionFactory.java index 19cf40de13..08c7a43abf 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RoutingConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RoutingConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-2025 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. @@ -16,7 +16,7 @@ package org.springframework.amqp.rabbit.connection; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Implementations select a connection factory based on a supplied key. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleConnection.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleConnection.java index 71515a26ad..40d31d53d9 100755 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleConnection.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleConnection.java @@ -19,13 +19,12 @@ import java.io.IOException; import java.net.InetAddress; -import javax.annotation.Nullable; - import com.rabbitmq.client.AlreadyClosedException; import com.rabbitmq.client.BlockedListener; import com.rabbitmq.client.Channel; import com.rabbitmq.client.impl.NetworkConnection; import com.rabbitmq.client.impl.recovery.AutorecoveringConnection; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpResourceNotAvailableException; import org.springframework.amqp.AmqpTimeoutException; @@ -54,9 +53,9 @@ public class SimpleConnection implements Connection, NetworkConnection { @Nullable private final BackOffExecution backOffExecution; - public SimpleConnection(com.rabbitmq.client.Connection delegate, int closeTimeout) { - this(delegate, closeTimeout, null); - } + public SimpleConnection(com.rabbitmq.client.Connection delegate, int closeTimeout) { + this(delegate, closeTimeout, null); + } /** * Construct an instance with the {@link org.springframework.util.backoff.BackOffExecution} arguments. @@ -66,7 +65,8 @@ public SimpleConnection(com.rabbitmq.client.Connection delegate, int closeTimeou * @since 3.1.3 */ public SimpleConnection(com.rabbitmq.client.Connection delegate, int closeTimeout, - @Nullable BackOffExecution backOffExecution) { + @Nullable BackOffExecution backOffExecution) { + this.delegate = delegate; this.closeTimeout = closeTimeout; this.backOffExecution = backOffExecution; @@ -135,10 +135,9 @@ public boolean isOpen() { if (!this.explicitlyClosed && this.delegate instanceof AutorecoveringConnection && !this.delegate.isOpen()) { throw new AutoRecoverConnectionNotCurrentlyOpenException("Auto recovery connection is not currently open"); } - return this.delegate != null && (this.delegate.isOpen()); + return this.delegate.isOpen(); } - @Override public int getLocalPort() { if (this.delegate instanceof NetworkConnection networkConn) { @@ -158,7 +157,7 @@ public boolean removeBlockedListener(BlockedListener listener) { } @Override - public InetAddress getLocalAddress() { + public @Nullable InetAddress getLocalAddress() { if (this.delegate instanceof NetworkConnection networkConn) { return networkConn.getLocalAddress(); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimplePropertyValueConnectionNameStrategy.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimplePropertyValueConnectionNameStrategy.java index 1a8287d9af..f4eebedd0f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimplePropertyValueConnectionNameStrategy.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimplePropertyValueConnectionNameStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2025 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. @@ -16,6 +16,8 @@ package org.springframework.amqp.rabbit.connection; +import org.jspecify.annotations.Nullable; + import org.springframework.context.EnvironmentAware; import org.springframework.core.env.Environment; import org.springframework.util.Assert; @@ -32,9 +34,9 @@ public class SimplePropertyValueConnectionNameStrategy implements ConnectionName private final String propertyName; - private String propertyValue; + private @Nullable String propertyValue; - private Environment environment; + private @Nullable Environment environment; public SimplePropertyValueConnectionNameStrategy(String propertyName) { Assert.notNull(propertyName, "'propertyName' cannot be null"); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleResourceHolder.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleResourceHolder.java index 10036d3ad2..a31e0a2534 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleResourceHolder.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleResourceHolder.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 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. @@ -24,9 +24,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.NamedThreadLocal; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -46,6 +46,7 @@ * @author Artem Bilan * @author Gary Russell * @author Ngoc Nhan + * * @since 1.3 */ public final class SimpleResourceHolder { @@ -56,10 +57,10 @@ public final class SimpleResourceHolder { private static final Log LOGGER = LogFactory.getLog(SimpleResourceHolder.class); - private static final ThreadLocal> RESOURCES = + private static final ThreadLocal<@Nullable Map> RESOURCES = new NamedThreadLocal<>("Simple resources"); - private static final ThreadLocal>> STACK = + private static final ThreadLocal<@Nullable Map>> STACK = new NamedThreadLocal<>("Simple resources"); /** @@ -92,8 +93,7 @@ public static boolean has(Object key) { * @return a value bound to the current thread (usually the active * resource object), or null if none */ - @Nullable - public static Object get(Object key) { + public static @Nullable Object get(Object key) { Object value = doGet(key); if (value != null && LOGGER.isTraceEnabled()) { LOGGER.trace("Retrieved value [" + value + FOR_KEY + key + BOUND_TO_THREAD @@ -107,8 +107,7 @@ public static Object get(Object key) { * @param actualKey the key. * @return the resource object. */ - @Nullable - private static Object doGet(Object actualKey) { + private static @Nullable Object doGet(Object actualKey) { Map map = RESOURCES.get(); if (map == null) { return null; @@ -152,7 +151,7 @@ public static void push(Object key, Object value) { bind(key, value); } else { - Map> stack = STACK.get(); + Map> stack = STACK.get(); if (stack == null) { stack = new HashMap<>(); STACK.set(stack); @@ -170,12 +169,11 @@ public static void push(Object key, Object value) { * @return the popped value. * @since 2.1.11 */ - @Nullable public static Object pop(Object key) { Object popped = unbind(key); - Map> stack = STACK.get(); + Map> stack = STACK.get(); if (stack != null) { - Deque deque = stack.get(key); + Deque<@Nullable Object> deque = stack.get(key); if (deque != null && !deque.isEmpty()) { Object previousValue = deque.pop(); if (previousValue != null) { @@ -207,8 +205,7 @@ public static Object unbind(Object key) throws IllegalStateException { * @param key the key to unbind (usually the resource factory) * @return the previously bound value, or null if none bound */ - @Nullable - public static Object unbindIfPossible(Object key) { + public static @Nullable Object unbindIfPossible(Object key) { Map map = RESOURCES.get(); if (map == null) { return null; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleRoutingConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleRoutingConnectionFactory.java index 39a5bd3f24..cb11384782 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleRoutingConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/SimpleRoutingConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 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. @@ -16,7 +16,7 @@ package org.springframework.amqp.rabbit.connection; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * An {@link AbstractRoutingConnectionFactory} implementation which gets a {@code lookupKey} @@ -25,13 +25,13 @@ * * @author Artem Bilan * @author Gary Russell + * * @since 1.3 */ public class SimpleRoutingConnectionFactory extends AbstractRoutingConnectionFactory { @Override - @Nullable - protected Object determineCurrentLookupKey() { + protected @Nullable Object determineCurrentLookupKey() { return SimpleResourceHolder.get(this); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java index 1c2702df5c..8795a5e509 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/ThreadChannelConnectionFactory.java @@ -30,13 +30,13 @@ import com.rabbitmq.client.ShutdownListener; import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInterceptor; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpException; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.support.NameMatchMethodPointcutAdvisor; import org.springframework.context.SmartLifecycle; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -48,6 +48,8 @@ * @author Leonardo Ferreira * @author Christian Tzolov * @author Ngoc Nhan + * @author Artem Bilan + * * @since 2.3 * */ @@ -62,7 +64,7 @@ public class ThreadChannelConnectionFactory extends AbstractConnectionFactory private final AtomicBoolean running = new AtomicBoolean(); - private volatile ConnectionWrapper connection; + private volatile @Nullable ConnectionWrapper connection; private boolean simplePublisherConfirms; @@ -81,6 +83,7 @@ public ThreadChannelConnectionFactory(ConnectionFactory rabbitConnectionFactory) * @param rabbitConnectionFactory the rabbitmq connection factory. * @param isPublisher true if we are creating a publisher connection factory. */ + @SuppressWarnings("this-escape") private ThreadChannelConnectionFactory(ConnectionFactory rabbitConnectionFactory, boolean isPublisher) { super(rabbitConnectionFactory); if (!isPublisher) { @@ -106,11 +109,12 @@ public boolean isSimplePublisherConfirms() { * Enable simple publisher confirms. * @param simplePublisherConfirms true to enable. */ + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void setSimplePublisherConfirms(boolean simplePublisherConfirms) { this.simplePublisherConfirms = simplePublisherConfirms; if (this.defaultPublisherFactory) { ((ThreadChannelConnectionFactory) getPublisherConnectionFactory()) - .setSimplePublisherConfirms(simplePublisherConfirms); // NOSONAR + .setSimplePublisherConfirms(simplePublisherConfirms); // NOSONAR } } @@ -139,34 +143,40 @@ public boolean isRunning() { public void addConnectionListener(ConnectionListener listener) { super.addConnectionListener(listener); // handles publishing sub-factory // If the connection is already alive we assume that the new listener wants to be notified - if (this.connection != null && this.connection.isOpen()) { - listener.onCreate(this.connection); + ConnectionWrapper connectionWrapper = this.connection; + if (connectionWrapper != null && connectionWrapper.isOpen()) { + listener.onCreate(connectionWrapper); } } @Override public Connection createConnection() throws AmqpException { - this.lock.lock(); - try { - if (this.connection == null || !this.connection.isOpen()) { - Connection bareConnection = createBareConnection(); // NOSONAR - see destroy() - this.connection = new ConnectionWrapper(bareConnection.getDelegate(), getCloseTimeout()); // NOSONAR - getConnectionListener().onCreate(this.connection); + ConnectionWrapper connectionWrapper = this.connection; + if (connectionWrapper == null || !connectionWrapper.isOpen()) { + this.lock.lock(); + try { + connectionWrapper = this.connection; + if (connectionWrapper == null || !connectionWrapper.isOpen()) { + Connection bareConnection = createBareConnection(); + connectionWrapper = new ConnectionWrapper(bareConnection.getDelegate(), getCloseTimeout()); + this.connection = connectionWrapper; + getConnectionListener().onCreate(this.connection); + } + } + finally { + this.lock.unlock(); } - return this.connection; - } - finally { - this.lock.unlock(); } + return connectionWrapper; } /** * Close the channel associated with this thread, if any. */ public void closeThreadChannel() { - ConnectionWrapper connection2 = this.connection; - if (connection2 != null) { - connection2.closeThreadChannel(); + ConnectionWrapper connectionWrapper = this.connection; + if (connectionWrapper != null) { + connectionWrapper.closeThreadChannel(); } } @@ -186,8 +196,9 @@ public void destroy() { this.lock.lock(); try { super.destroy(); - if (this.connection != null) { - this.connection.forceClose(); + ConnectionWrapper connectionWrapper = this.connection; + if (connectionWrapper != null) { + connectionWrapper.forceClose(); this.connection = null; } if (!this.switchesInProgress.isEmpty() && this.logger.isWarnEnabled()) { @@ -212,11 +223,11 @@ public void destroy() { * @since 2.3.7 * @see #switchContext(Object) */ - @Nullable - public Object prepareSwitchContext() { + public @Nullable Object prepareSwitchContext() { return prepareSwitchContext(UUID.randomUUID()); } + @SuppressWarnings("resource") @Nullable Object prepareSwitchContext(UUID uuid) { Object pubContext = null; @@ -224,11 +235,11 @@ Object prepareSwitchContext(UUID uuid) { pubContext = tccf.prepareSwitchContext(uuid); } Context context = ((ConnectionWrapper) createConnection()).prepareSwitchContext(); - if (context.getNonTx() == null && context.getTx() == null) { + if (context.nonTx() == null && context.tx() == null) { this.logger.debug("No channels are bound to this thread"); return pubContext; } - if (this.switchesInProgress.values().contains(Thread.currentThread())) { + if (this.switchesInProgress.containsValue(Thread.currentThread())) { this.logger .warn("A previous context switch from this thread has not been claimed yet; possible memory leak?"); } @@ -253,6 +264,7 @@ public void switchContext(@Nullable Object toSwitch) { } } + @SuppressWarnings("resource") boolean doSwitch(Object toSwitch) { boolean switched = false; if (getPublisherConnectionFactory() instanceof ThreadChannelConnectionFactory tccf) { @@ -272,9 +284,9 @@ private final class ConnectionWrapper extends SimpleConnection { /* * Intentionally not static. */ - private final ThreadLocal channels = new ThreadLocal<>(); + private final ThreadLocal<@Nullable Channel> channels = new ThreadLocal<>(); - private final ThreadLocal txChannels = new ThreadLocal<>(); + private final ThreadLocal<@Nullable Channel> txChannels = new ThreadLocal<>(); ConnectionWrapper(com.rabbitmq.client.Connection delegate, int closeTimeout) { super(delegate, closeTimeout); @@ -372,7 +384,7 @@ public void closeThreadChannel() { doClose(this.txChannels); } - private void doClose(ThreadLocal channelsTL) { + private void doClose(ThreadLocal<@Nullable Channel> channelsTL) { Channel channel = channelsTL.get(); if (channel != null) { channelsTL.remove(); @@ -407,17 +419,17 @@ Context prepareSwitchContext() { } void switchContext(Context context) { - Channel nonTx = context.getNonTx(); + Channel nonTx = context.nonTx(); if (nonTx != null) { doSwitch(nonTx, this.channels); } - Channel tx = context.getTx(); + Channel tx = context.tx(); if (tx != null) { doSwitch(tx, this.txChannels); } } - private void doSwitch(Channel channel, ThreadLocal channelTL) { + private void doSwitch(Channel channel, ThreadLocal<@Nullable Channel> channelTL) { Channel toClose = channelTL.get(); if (toClose != null) { RabbitUtils.setPhysicalCloseRequired(channel, true); @@ -428,26 +440,7 @@ private void doSwitch(Channel channel, ThreadLocal channelTL) { } - private static class Context { - - private final Channel nonTx; - - private final Channel tx; - - Context(@Nullable Channel nonTx, @Nullable Channel tx) { - this.nonTx = nonTx; - this.tx = tx; - } - - @Nullable - Channel getNonTx() { - return this.nonTx; - } - - @Nullable - Channel getTx() { - return this.tx; - } + private record Context(@Nullable Channel nonTx, @Nullable Channel tx) { } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/WebFluxNodeLocator.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/WebFluxNodeLocator.java index 75acd71e81..568136ad74 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/WebFluxNodeLocator.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/WebFluxNodeLocator.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 the original author or authors. + * Copyright 2022-2025 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. @@ -23,9 +23,10 @@ import java.util.HashMap; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.MediaType; -import org.springframework.lang.Nullable; import org.springframework.web.reactive.function.client.ExchangeFilterFunctions; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.util.UriUtils; @@ -41,8 +42,7 @@ public class WebFluxNodeLocator implements NodeLocator { @Override - @Nullable - public Map restCall(WebClient client, String baseUri, String vhost, String queue) + public @Nullable Map restCall(WebClient client, String baseUri, String vhost, String queue) throws URISyntaxException { URI uri = new URI(baseUri) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/package-info.java index de2b6135d5..354c068b0a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/package-info.java @@ -1,5 +1,5 @@ /** * Provides classes related to connections. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.connection; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/AmqpNackReceivedException.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/AmqpNackReceivedException.java index fb8456e004..34f2a18c74 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/AmqpNackReceivedException.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/AmqpNackReceivedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2025 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. @@ -16,6 +16,8 @@ package org.springframework.amqp.rabbit.core; +import java.io.Serial; + import org.springframework.amqp.AmqpException; import org.springframework.amqp.core.Message; @@ -29,6 +31,7 @@ */ public class AmqpNackReceivedException extends AmqpException { + @Serial private static final long serialVersionUID = 1L; private final Message failedMessage; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplate.java index 18e0b11719..4a5eb5111d 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 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. @@ -21,13 +21,14 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.AmqpException; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.batch.BatchingStrategy; import org.springframework.amqp.rabbit.batch.MessageBatch; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.connection.CorrelationData; -import org.springframework.lang.Nullable; import org.springframework.scheduling.TaskScheduler; /** @@ -51,7 +52,7 @@ public class BatchingRabbitTemplate extends RabbitTemplate { private final TaskScheduler scheduler; - private volatile ScheduledFuture scheduledTask; + private volatile @Nullable ScheduledFuture scheduledTask; /** * Create an instance with the supplied parameters. @@ -79,7 +80,7 @@ public BatchingRabbitTemplate(ConnectionFactory connectionFactory, BatchingStrat } @Override - public void send(String exchange, String routingKey, Message message, + public void send(@Nullable String exchange, @Nullable String routingKey, Message message, @Nullable CorrelationData correlationData) throws AmqpException { this.lock.lock(); try { @@ -95,7 +96,7 @@ public void send(String exchange, String routingKey, Message message, } MessageBatch batch = this.batchingStrategy.addToBatch(exchange, routingKey, message); if (batch != null) { - super.send(batch.getExchange(), batch.getRoutingKey(), batch.getMessage(), null); + super.send(batch.exchange(), batch.routingKey(), batch.message(), null); } Date next = this.batchingStrategy.nextRelease(); if (next != null) { @@ -119,7 +120,7 @@ private void releaseBatches() { this.lock.lock(); try { for (MessageBatch batch : this.batchingStrategy.releaseBatches()) { - super.send(batch.getExchange(), batch.getRoutingKey(), batch.getMessage(), null); + super.send(batch.exchange(), batch.routingKey(), batch.message(), null); } } finally { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BrokerEvent.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BrokerEvent.java index b30cdc7a02..761ea35850 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BrokerEvent.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BrokerEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * Copyright 2018-2025 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. @@ -18,15 +18,19 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.event.AmqpEvent; import org.springframework.util.Assert; /** - * Represents a broker event generated by the Event Exchange Plugin - * (https://www.rabbitmq.com/event-exchange.html). + * Represents a broker event generated by the + * Event Exchange Plugin. * * @author Gary Russell + * @author Artem Bilan + * * @since 2.1 * */ @@ -50,7 +54,7 @@ public BrokerEvent(Object source, MessageProperties properties) { * The event type ({@link MessageProperties#getReceivedRoutingKey()}). * @return the type. */ - public String getEventType() { + public @Nullable String getEventType() { return this.properties.getReceivedRoutingKey(); } @@ -58,7 +62,7 @@ public String getEventType() { * Properties of the event {@link MessageProperties#getHeaders()}. * @return the properties. */ - public Map getEventProperties() { + public Map getEventProperties() { return this.properties.getHeaders(); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BrokerEventListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BrokerEventListener.java index 8c5113e87e..0436ce5c87 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BrokerEventListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/BrokerEventListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 the original author or authors. + * Copyright 2018-2025 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. @@ -22,6 +22,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.AnonymousQueue; import org.springframework.amqp.core.Base64UrlNamingStrategy; @@ -39,20 +40,20 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.context.SmartLifecycle; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; /** - * When the event-exchange-plugin is enabled (see - * https://www.rabbitmq.com/event-exchange.html), if an object of this type is declared as - * a bean, selected events will be published as {@link BrokerEvent}s. Such events can then - * be consumed using an {@code ApplicationListener} or {@code @EventListener} method. + * When the Event Exchange Plugin is enabled, + * if an object of this type is declared as a bean, selected events will be published as {@link BrokerEvent}s. + * Such events can then be consumed using an {@code ApplicationListener} or {@code @EventListener} method. * An {@link AnonymousQueue} will be bound to the {@code amq.rabbitmq.event} topic exchange * with the supplied keys. * * @author Gary Russell * @author Christian Tzolov + * @author Artem Bilan + * * @since 2.1 * */ @@ -81,9 +82,9 @@ public class BrokerEventListener implements MessageListener, ApplicationEventPub private boolean stopInvoked; - private Exception bindingsFailedException; + private @Nullable Exception bindingsFailedException; - private ApplicationEventPublisher applicationEventPublisher; + private @Nullable ApplicationEventPublisher applicationEventPublisher; /** * Construct an instance using the supplied connection factory and event keys. Event @@ -96,6 +97,7 @@ public class BrokerEventListener implements MessageListener, ApplicationEventPub * @param connectionFactory the connection factory. * @param eventKeys the event keys. */ + @SuppressWarnings("this-escape") public BrokerEventListener(ConnectionFactory connectionFactory, String... eventKeys) { this(new DirectMessageListenerContainer(connectionFactory), true, eventKeys); } @@ -116,6 +118,7 @@ public BrokerEventListener(AbstractMessageListenerContainer container, String... this(container, false, eventKeys); } + @SuppressWarnings("this-escape") private BrokerEventListener(AbstractMessageListenerContainer container, boolean ownContainer, String... eventKeys) { Assert.notNull(container, "listener container cannot be null"); Assert.isTrue(!ObjectUtils.isEmpty(eventKeys), "At least one event key is required"); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/ChannelCallback.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/ChannelCallback.java index 0b24fcc3a8..7a8d14868e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/ChannelCallback.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/ChannelCallback.java @@ -17,8 +17,7 @@ package org.springframework.amqp.rabbit.core; import com.rabbitmq.client.Channel; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Basic callback for use in RabbitTemplate. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/CorrelationDataPostProcessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/CorrelationDataPostProcessor.java index 0b5d0456f2..d3073e75ee 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/CorrelationDataPostProcessor.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/CorrelationDataPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2019 the original author or authors. + * Copyright 2017-2025 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. @@ -16,6 +16,8 @@ package org.springframework.amqp.rabbit.core; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.connection.CorrelationData; @@ -25,6 +27,8 @@ * {@link org.springframework.amqp.core.MessagePostProcessor}s. * * @author Gary Russell + * @author Artem Bilan + * * @since 1.6.7 * */ @@ -37,6 +41,6 @@ public interface CorrelationDataPostProcessor { * @param correlationData the existing data (if present). * @return the correlation data. */ - CorrelationData postProcess(Message message, CorrelationData correlationData); + CorrelationData postProcess(Message message, @Nullable CorrelationData correlationData); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/DeclarationExceptionEvent.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/DeclarationExceptionEvent.java index e07abfe010..9ed783a6aa 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/DeclarationExceptionEvent.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/DeclarationExceptionEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 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. @@ -16,8 +16,11 @@ package org.springframework.amqp.rabbit.core; +import java.io.Serial; + +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Declarable; -import org.springframework.lang.Nullable; /** * Application event published when a declaration exception occurs. @@ -28,9 +31,10 @@ */ public class DeclarationExceptionEvent extends RabbitAdminEvent { + @Serial private static final long serialVersionUID = -8367796410619780665L; - private final transient Declarable declarable; + private final transient @Nullable Declarable declarable; private final Throwable throwable; @@ -43,8 +47,7 @@ public DeclarationExceptionEvent(Object source, @Nullable Declarable declarable, /** * @return the declarable - if null, we were declaring a broker-named queue. */ - @Nullable - public Declarable getDeclarable() { + public @Nullable Declarable getDeclarable() { return this.declarable; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/DeclareExchangeConnectionListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/DeclareExchangeConnectionListener.java index f814835cc7..58fabc24f6 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/DeclareExchangeConnectionListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/DeclareExchangeConnectionListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 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. @@ -16,6 +16,8 @@ package org.springframework.amqp.rabbit.core; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.Exchange; import org.springframework.amqp.rabbit.connection.Connection; import org.springframework.amqp.rabbit.connection.ConnectionListener; @@ -25,6 +27,8 @@ * connection is established. * * @author Gary Russell + * @author Artem Bilan + * * @since 1.5.4 * */ @@ -40,16 +44,13 @@ public DeclareExchangeConnectionListener(Exchange exchange, RabbitAdmin admin) { } @Override - public void onCreate(Connection connection) { + public void onCreate(@Nullable Connection connection) { try { this.admin.declareExchange(this.exchange); } catch (Exception e) { + // Ignoire } } - @Override - public void onClose(Connection connection) { - } - } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java index f94f6bf573..d04a61f254 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java @@ -39,6 +39,7 @@ import com.rabbitmq.client.Channel; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpException; import org.springframework.amqp.core.AmqpAdmin; @@ -63,7 +64,6 @@ import org.springframework.core.task.TaskExecutor; import org.springframework.jmx.export.annotation.ManagedOperation; import org.springframework.jmx.export.annotation.ManagedResource; -import org.springframework.lang.Nullable; import org.springframework.retry.backoff.ExponentialBackOffPolicy; import org.springframework.retry.policy.SimpleRetryPolicy; import org.springframework.retry.support.RetryTemplate; @@ -136,19 +136,20 @@ public class RabbitAdmin implements AmqpAdmin, ApplicationContextAware, Applicat private final Lock manualDeclarablesLock = new ReentrantLock(); + @SuppressWarnings("NullAway.Init") private String beanName; - private RetryTemplate retryTemplate; + private @Nullable RetryTemplate retryTemplate; private boolean retryDisabled; private boolean autoStartup = true; - private ApplicationContext applicationContext; + private @Nullable ApplicationContext applicationContext; private boolean ignoreDeclarationExceptions; - private ApplicationEventPublisher applicationEventPublisher; + private @Nullable ApplicationEventPublisher applicationEventPublisher; private TaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(); @@ -158,7 +159,7 @@ public class RabbitAdmin implements AmqpAdmin, ApplicationContextAware, Applicat private volatile boolean running = false; - private volatile DeclarationExceptionEvent lastDeclarationExceptionEvent; + private volatile @Nullable DeclarationExceptionEvent lastDeclarationExceptionEvent; /** * Construct an instance using the provided {@link ConnectionFactory}. @@ -205,10 +206,9 @@ public void setIgnoreDeclarationExceptions(boolean ignoreDeclarationExceptions) /** * @return the last {@link DeclarationExceptionEvent} that was detected in this admin. - * * @since 1.6 */ - public DeclarationExceptionEvent getLastDeclarationExceptionEvent() { + public @Nullable DeclarationExceptionEvent getLastDeclarationExceptionEvent() { return this.lastDeclarationExceptionEvent; } @@ -247,8 +247,9 @@ public void declareExchange(final Exchange exchange) { @Override @ManagedOperation(description = "Delete an exchange from the broker") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public boolean deleteExchange(final String exchangeName) { - return this.rabbitTemplate.execute(channel -> { // NOSONAR never returns null + return this.rabbitTemplate.execute(channel -> { if (isDeletingDefaultExchange(exchangeName)) { return true; } @@ -264,6 +265,7 @@ public boolean deleteExchange(final String exchangeName) { }); } + @SuppressWarnings("NullAway") // Dataflow analysis limitation private void removeExchangeBindings(final String exchangeName) { this.manualDeclarablesLock.lock(); try { @@ -281,7 +283,6 @@ private void removeExchangeBindings(final String exchangeName) { } } - // Queue operations /** @@ -296,8 +297,7 @@ private void removeExchangeBindings(final String exchangeName) { */ @Override @ManagedOperation(description = "Declare a queue on the broker (this operation is not available remotely)") - @Nullable - public String declareQueue(final Queue queue) { + public @Nullable String declareQueue(final Queue queue) { try { return this.rabbitTemplate.execute(channel -> { DeclareOk[] declared = declareQueues(channel, queue); @@ -324,8 +324,8 @@ public String declareQueue(final Queue queue) { @Override @ManagedOperation(description = "Declare a queue with a broker-generated name (this operation is not available remotely)") - @Nullable - public Queue declareQueue() { + @SuppressWarnings("NullAway") // Dataflow analysis limitation + public @Nullable Queue declareQueue() { try { DeclareOk declareOk = this.rabbitTemplate.execute(Channel::queueDeclare); return new Queue(declareOk.getQueue(), false, true, true); // NOSONAR never null @@ -338,8 +338,9 @@ public Queue declareQueue() { @Override @ManagedOperation(description = "Delete a queue from the broker") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public boolean deleteQueue(final String queueName) { - return this.rabbitTemplate.execute(channel -> { // NOSONAR never returns null + return this.rabbitTemplate.execute(channel -> { try { channel.queueDelete(queueName); removeQueueBindings(queueName); @@ -362,6 +363,7 @@ public void deleteQueue(final String queueName, final boolean unused, final bool }); } + @SuppressWarnings("NullAway") // Dataflow analysis limitation private void removeQueueBindings(final String queueName) { this.manualDeclarablesLock.lock(); try { @@ -392,6 +394,7 @@ public void purgeQueue(final String queueName, final boolean noWait) { @Override @ManagedOperation(description = "Purge a queue and return the number of messages purged") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public int purgeQueue(final String queueName) { return this.rabbitTemplate.execute(channel -> { // NOSONAR never returns null PurgeOk queuePurged = channel.queuePurge(queueName); @@ -436,7 +439,7 @@ public void removeBinding(final Binding binding) { channel.exchangeUnbind(binding.getDestination(), binding.getExchange(), binding.getRoutingKey(), binding.getArguments()); } - this.manualDeclarables.remove(binding.toString()); + this.manualDeclarables.remove(binding); return null; }); } @@ -447,7 +450,7 @@ public void removeBinding(final Binding binding) { */ @Override @ManagedOperation(description = "Get queue name, message count and consumer count") - public Properties getQueueProperties(final String queueName) { + public @Nullable Properties getQueueProperties(final String queueName) { QueueInformation queueInfo = getQueueInfo(queueName); if (queueInfo != null) { Properties props = new Properties(); @@ -462,7 +465,7 @@ public Properties getQueueProperties(final String queueName) { } @Override - public QueueInformation getQueueInfo(String queueName) { + public @Nullable QueueInformation getQueueInfo(String queueName) { Assert.hasText(queueName, "'queueName' cannot be null or empty"); return this.rabbitTemplate.execute(channel -> { try { @@ -481,6 +484,7 @@ public QueueInformation getQueueInfo(String queueName) { } } catch (@SuppressWarnings(UNUSED) TimeoutException e1) { + // Ignore } return null; } @@ -506,7 +510,7 @@ public void setExplicitDeclarationsOnly(boolean explicitDeclarationsOnly) { /** * Normally, when a connection is recovered, the admin only recovers auto-delete queues, - * etc, that are declared as beans in the application context. When this is true, it + * etc., that are declared as beans in the application context. When this is true, it * will also redeclare any manually declared {@link Declarable}s via admin methods. * @return true to redeclare. * @since 2.4 @@ -647,7 +651,6 @@ public void afterPropertiesSet() { */ @Override // NOSONAR complexity public void initialize() { - redeclareBeanDeclarables(); redeclareManualDeclarables(); } @@ -757,6 +760,7 @@ public Set getManualDeclarableSet() { private void processDeclarables(Collection contextExchanges, Collection contextQueues, Collection contextBindings) { + @SuppressWarnings("NullAway") // Dataflow analysis limitation Collection declarables = this.applicationContext.getBeansOfType(Declarables.class, false, true) .values(); declarables.forEach(d -> { @@ -782,7 +786,7 @@ else if (declarable instanceof Binding binding) { * @return a new collection containing {@link Declarable}s that should be declared by this * admin. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({ "unchecked", "NullAway" }) // Dataflow analysis limitation private Collection filterDeclarables(Collection declarables, Collection customizers) { @@ -802,7 +806,7 @@ private Collection filterDeclarables(Collection dec private boolean declarableByMe(T dec) { return (dec.getDeclaringAdmins().isEmpty() && !this.explicitDeclarationsOnly) // NOSONAR boolean complexity || dec.getDeclaringAdmins().contains(this) - || (this.beanName != null && dec.getDeclaringAdmins().contains(this.beanName)); + || dec.getDeclaringAdmins().contains(this.beanName); } // private methods for declaring Exchanges, Queues, and Bindings on a Channel @@ -981,6 +985,7 @@ private boolean isRemovingImplicitQueueBinding(Binding binding) { return false; } + @SuppressWarnings("NullAway") // Dataflow analysis limitation private boolean isImplicitQueueBinding(Binding binding) { return isDefaultExchange(binding.getExchange()) && binding.getDestination().equals(binding.getRoutingKey()); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdminEvent.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdminEvent.java index d7d50fb16d..6ba0913ea2 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdminEvent.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdminEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 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. @@ -16,6 +16,8 @@ package org.springframework.amqp.rabbit.core; +import java.io.Serial; + import org.springframework.amqp.event.AmqpEvent; /** @@ -27,6 +29,7 @@ */ public class RabbitAdminEvent extends AmqpEvent { + @Serial private static final long serialVersionUID = 1L; public RabbitAdminEvent(Object source) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitGatewaySupport.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitGatewaySupport.java index 406520a82e..f81ecfc37f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitGatewaySupport.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitGatewaySupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -18,11 +18,11 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.beans.factory.BeanInitializationException; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; /** * Convenient super class for application classes that need RabbitMQ access. @@ -45,7 +45,7 @@ public class RabbitGatewaySupport implements InitializingBean { /** Logger available to subclasses. */ protected final Log logger = LogFactory.getLog(getClass()); // NOSONAR - private RabbitOperations rabbitOperations; + private @Nullable RabbitOperations rabbitOperations; /** * Set the Rabbit connection factory to be used by the gateway. @@ -73,8 +73,7 @@ protected RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactor /** * @return The Rabbit ConnectionFactory used by the gateway. */ - @Nullable - public final ConnectionFactory getConnectionFactory() { + public final @Nullable ConnectionFactory getConnectionFactory() { return (this.rabbitOperations != null ? this.rabbitOperations.getConnectionFactory() : null); } @@ -90,7 +89,7 @@ public final void setRabbitOperations(RabbitOperations rabbitOperations) { /** * @return The {@link RabbitOperations} for the gateway. */ - public final RabbitOperations getRabbitOperations() { + public final @Nullable RabbitOperations getRabbitOperations() { return this.rabbitOperations; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitMessageOperations.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitMessageOperations.java index 5b62f7a42c..8b3d9baa90 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitMessageOperations.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitMessageOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -18,6 +18,8 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; import org.springframework.messaging.core.MessagePostProcessor; @@ -100,8 +102,8 @@ void convertAndSend(String exchange, String routingKey, Object payload, MessageP * @param postProcessor the post processor to apply to the message * @throws MessagingException a messaging exception. */ - void convertAndSend(String exchange, String routingKey, Object payload, Map headers, MessagePostProcessor postProcessor) throws MessagingException; + void convertAndSend(String exchange, String routingKey, Object payload, + @Nullable Map headers, @Nullable MessagePostProcessor postProcessor) throws MessagingException; /** * Send a request message to a specific exchange with a specific routing key and @@ -113,6 +115,7 @@ void convertAndSend(String exchange, String routingKey, Object payload, Map sendAndReceive(String exchange, String routingKey, Message requestMessage) throws MessagingException; /** @@ -129,7 +132,7 @@ void convertAndSend(String exchange, String routingKey, Object payload, Map T convertSendAndReceive(String exchange, String routingKey, Object request, Class targetClass) + @Nullable T convertSendAndReceive(String exchange, String routingKey, Object request, Class targetClass) throws MessagingException; /** @@ -148,8 +151,8 @@ T convertSendAndReceive(String exchange, String routingKey, Object request, * could not be received, for example due to a timeout * @throws MessagingException a messaging exception. */ - T convertSendAndReceive(String exchange, String routingKey, Object request, Map headers, - Class targetClass) throws MessagingException; + @Nullable T convertSendAndReceive(String exchange, String routingKey, Object request, + @Nullable Map headers, Class targetClass) throws MessagingException; /** * Convert the given request Object to serialized form, possibly using a @@ -167,15 +170,15 @@ T convertSendAndReceive(String exchange, String routingKey, Object request, * could not be received, for example due to a timeout * @throws MessagingException a messaging exception. */ - T convertSendAndReceive(String exchange, String routingKey, Object request, Class targetClass, - MessagePostProcessor requestPostProcessor) throws MessagingException; + @Nullable T convertSendAndReceive(String exchange, String routingKey, Object request, Class targetClass, + @Nullable MessagePostProcessor requestPostProcessor) throws MessagingException; /** * Convert the given request Object to serialized form, possibly using a * {@link org.springframework.messaging.converter.MessageConverter}, * wrap it as a message with the given headers, apply the given post processor * and send the resulting {@link Message} to a specific exchange with a - * specific routing key,, receive the reply and convert its body of the + * specific routing key, receive the reply and convert its body of the * given target class. * @param exchange the name of the exchange * @param routingKey the routing key @@ -188,7 +191,8 @@ T convertSendAndReceive(String exchange, String routingKey, Object request, * could not be received, for example due to a timeout * @throws MessagingException a messaging exception. */ - T convertSendAndReceive(String exchange, String routingKey, Object request, Map headers, - Class targetClass, MessagePostProcessor requestPostProcessor) throws MessagingException; + @Nullable T convertSendAndReceive(String exchange, String routingKey, Object request, + @Nullable Map headers, Class targetClass, + @Nullable MessagePostProcessor requestPostProcessor) throws MessagingException; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplate.java index 304200fea7..4cc3330847 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 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. @@ -18,13 +18,14 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.rabbit.connection.CorrelationData; import org.springframework.amqp.support.AmqpHeaders; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.amqp.support.converter.MessagingMessageConverter; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; import org.springframework.messaging.converter.MessageConversionException; @@ -42,6 +43,7 @@ public class RabbitMessagingTemplate extends AbstractMessagingTemplate implements RabbitMessageOperations, InitializingBean { + @SuppressWarnings("NullAway.Init") private RabbitTemplate rabbitTemplate; private MessageConverter amqpMessageConverter = new MessagingMessageConverter(); @@ -50,7 +52,6 @@ public class RabbitMessagingTemplate extends AbstractMessagingTemplate private boolean useTemplateDefaultReceiveQueue; - /** * Constructor for use with bean properties. * Requires {@link #setRabbitTemplate} to be called. @@ -67,7 +68,6 @@ public RabbitMessagingTemplate(RabbitTemplate rabbitTemplate) { this.rabbitTemplate = rabbitTemplate; } - /** * Set the {@link RabbitTemplate} to use. * @param rabbitTemplate the template. @@ -126,7 +126,7 @@ public void setUseTemplateDefaultReceiveQueue(boolean useTemplateDefaultReceiveQ public void afterPropertiesSet() { Assert.notNull(getRabbitTemplate(), "Property 'rabbitTemplate' is required"); Assert.notNull(getAmqpMessageConverter(), "Property 'amqpMessageConverter' is required"); - if (!this.converterSet && this.rabbitTemplate.getMessageConverter() != null) { + if (!this.converterSet) { ((MessagingMessageConverter) this.amqpMessageConverter) .setPayloadConverter(this.rabbitTemplate.getMessageConverter()); } @@ -159,39 +159,35 @@ public void convertAndSend(String exchange, String routingKey, Object payload, @Override public void convertAndSend(String exchange, String routingKey, Object payload, @Nullable Map headers, @Nullable MessagePostProcessor postProcessor) - throws MessagingException { + throws MessagingException { Message message = doConvert(payload, headers, postProcessor); send(exchange, routingKey, message); } @Override - @Nullable - public Message sendAndReceive(String exchange, String routingKey, Message requestMessage) + public @Nullable Message sendAndReceive(String exchange, String routingKey, Message requestMessage) throws MessagingException { return doSendAndReceive(exchange, routingKey, requestMessage); } @Override - @Nullable - public T convertSendAndReceive(String exchange, String routingKey, Object request, + public @Nullable T convertSendAndReceive(String exchange, String routingKey, Object request, Class targetClass) throws MessagingException { return convertSendAndReceive(exchange, routingKey, request, null, targetClass); } @Override - @Nullable - public T convertSendAndReceive(String exchange, String routingKey, Object request, + public @Nullable T convertSendAndReceive(String exchange, String routingKey, Object request, @Nullable Map headers, Class targetClass) throws MessagingException { return convertSendAndReceive(exchange, routingKey, request, headers, targetClass, null); } @Override - @Nullable - public T convertSendAndReceive(String exchange, String routingKey, Object request, + public @Nullable T convertSendAndReceive(String exchange, String routingKey, Object request, Class targetClass, @Nullable MessagePostProcessor requestPostProcessor) throws MessagingException { return convertSendAndReceive(exchange, routingKey, request, null, targetClass, requestPostProcessor); @@ -199,8 +195,7 @@ public T convertSendAndReceive(String exchange, String routingKey, Object re @SuppressWarnings("unchecked") @Override - @Nullable - public T convertSendAndReceive(String exchange, String routingKey, Object request, + public @Nullable T convertSendAndReceive(String exchange, String routingKey, Object request, @Nullable Map headers, Class targetClass, @Nullable MessagePostProcessor requestPostProcessor) throws MessagingException { @@ -241,14 +236,12 @@ protected void doSend(String exchange, String routingKey, Message message) { } @Override - @Nullable - public Message receive() { + public @Nullable Message receive() { return doReceive(resolveDestination()); } @Override - @Nullable - public T receiveAndConvert(Class targetClass) { + public @Nullable T receiveAndConvert(Class targetClass) { return receiveAndConvert(resolveDestination(), targetClass); } @@ -264,7 +257,7 @@ private String resolveDestination() { } @Override - protected Message doReceive(String destination) { + protected @Nullable Message doReceive(String destination) { try { org.springframework.amqp.core.Message amqpMessage = this.rabbitTemplate.receive(destination); return convertAmqpMessage(amqpMessage); @@ -274,10 +267,8 @@ protected Message doReceive(String destination) { } } - @Override - @Nullable - protected Message doSendAndReceive(String destination, Message requestMessage) { + protected @Nullable Message doSendAndReceive(String destination, Message requestMessage) { try { org.springframework.amqp.core.Message amqpMessage = this.rabbitTemplate.sendAndReceive( destination, createMessage(requestMessage)); @@ -288,8 +279,7 @@ protected Message doSendAndReceive(String destination, Message requestMess } } - @Nullable - protected Message doSendAndReceive(String exchange, String routingKey, Message requestMessage) { + protected @Nullable Message doSendAndReceive(String exchange, String routingKey, Message requestMessage) { try { org.springframework.amqp.core.Message amqpMessage = this.rabbitTemplate.sendAndReceive( exchange, routingKey, createMessage(requestMessage)); @@ -309,8 +299,7 @@ private org.springframework.amqp.core.Message createMessage(Message message) } } - @Nullable - protected Message convertAmqpMessage(@Nullable org.springframework.amqp.core.Message message) { + protected @Nullable Message convertAmqpMessage(org.springframework.amqp.core.@Nullable Message message) { if (message == null) { return null; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitOperations.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitOperations.java index 0e895726f7..248e4306df 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitOperations.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 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. @@ -16,6 +16,8 @@ package org.springframework.amqp.rabbit.core; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.AmqpException; import org.springframework.amqp.core.AmqpTemplate; import org.springframework.amqp.core.Message; @@ -24,7 +26,6 @@ import org.springframework.amqp.rabbit.connection.CorrelationData; import org.springframework.context.Lifecycle; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.lang.Nullable; /** * Rabbit specific methods for Amqp functionality. @@ -37,19 +38,18 @@ public interface RabbitOperations extends AmqpTemplate, Lifecycle { /** - * Execute the callback with a channel and reliably close the channel afterwards. + * Execute the callback with a channel and reliably close the channel afterward. * @param action the call back. * @param the return type. * @return the result from the * {@link ChannelCallback#doInRabbit(com.rabbitmq.client.Channel)}. * @throws AmqpException if one occurs. */ - @Nullable - T execute(ChannelCallback action) throws AmqpException; + @Nullable T execute(ChannelCallback action) throws AmqpException; /** * Invoke the callback and run all operations on the template argument in a dedicated - * thread-bound channel and reliably close the channel afterwards. + * thread-bound channel and reliably close the channel afterward. * @param action the call back. * @param the return type. * @return the result from the @@ -57,8 +57,7 @@ public interface RabbitOperations extends AmqpTemplate, Lifecycle { * @throws AmqpException if one occurs. * @since 2.0 */ - @Nullable - default T invoke(OperationsCallback action) throws AmqpException { + default @Nullable T invoke(OperationsCallback action) throws AmqpException { return invoke(action, null, null); } @@ -72,9 +71,8 @@ default T invoke(OperationsCallback action) throws AmqpException { * @return the result of the action method. * @since 2.1 */ - @Nullable - T invoke(OperationsCallback action, @Nullable com.rabbitmq.client.ConfirmCallback acks, - @Nullable com.rabbitmq.client.ConfirmCallback nacks); + @Nullable T invoke(OperationsCallback action, com.rabbitmq.client.@Nullable ConfirmCallback acks, + com.rabbitmq.client.@Nullable ConfirmCallback nacks); /** * Delegate to the underlying dedicated channel to wait for confirms. The connection @@ -132,7 +130,7 @@ default void send(String routingKey, Message message, CorrelationData correlatio * @param correlationData data to correlate publisher confirms. * @throws AmqpException if there is a problem */ - void send(String exchange, String routingKey, Message message, CorrelationData correlationData) + void send(String exchange, String routingKey, Message message, @Nullable CorrelationData correlationData) throws AmqpException; /** @@ -206,7 +204,7 @@ void convertAndSend(String routingKey, Object message, MessagePostProcessor mess * @throws AmqpException if there is a problem */ void convertAndSend(String exchange, String routingKey, Object message, MessagePostProcessor messagePostProcessor, - CorrelationData correlationData) throws AmqpException; + @Nullable CorrelationData correlationData) throws AmqpException; /** * Basic RPC pattern with conversion. Send a Java object converted to a message to a @@ -269,7 +267,7 @@ Object convertSendAndReceive(String exchange, String routingKey, Object message, */ @Nullable Object convertSendAndReceive(Object message, MessagePostProcessor messagePostProcessor, - CorrelationData correlationData) throws AmqpException; + @Nullable CorrelationData correlationData) throws AmqpException; /** * Basic RPC pattern with conversion. Send a Java object converted to a message to a @@ -286,7 +284,7 @@ Object convertSendAndReceive(Object message, MessagePostProcessor messagePostPro */ @Nullable Object convertSendAndReceive(String routingKey, Object message, - MessagePostProcessor messagePostProcessor, CorrelationData correlationData) throws AmqpException; + MessagePostProcessor messagePostProcessor, @Nullable CorrelationData correlationData) throws AmqpException; /** * Basic RPC pattern with conversion. Send a Java object converted to a message to a @@ -304,7 +302,7 @@ Object convertSendAndReceive(String routingKey, Object message, */ @Nullable Object convertSendAndReceive(String exchange, String routingKey, Object message, - MessagePostProcessor messagePostProcessor, CorrelationData correlationData) + @Nullable MessagePostProcessor messagePostProcessor, @Nullable CorrelationData correlationData) throws AmqpException; /** @@ -322,8 +320,7 @@ Object convertSendAndReceive(String exchange, String routingKey, Object message, * @return the response if there is one. * @throws AmqpException if there is a problem. */ - @Nullable - T convertSendAndReceiveAsType(Object message, CorrelationData correlationData, + @Nullable T convertSendAndReceiveAsType(Object message, @Nullable CorrelationData correlationData, ParameterizedTypeReference responseType) throws AmqpException; /** @@ -342,9 +339,8 @@ T convertSendAndReceiveAsType(Object message, CorrelationData correlationDat * @return the response if there is one * @throws AmqpException if there is a problem */ - @Nullable - T convertSendAndReceiveAsType(String routingKey, Object message, CorrelationData correlationData, - ParameterizedTypeReference responseType) throws AmqpException; + @Nullable T convertSendAndReceiveAsType(String routingKey, Object message, + @Nullable CorrelationData correlationData, ParameterizedTypeReference responseType) throws AmqpException; /** * Basic RPC pattern with conversion. Send a Java object converted to a message to a @@ -363,10 +359,9 @@ T convertSendAndReceiveAsType(String routingKey, Object message, Correlation * @return the response if there is one * @throws AmqpException if there is a problem */ - @Nullable - default T convertSendAndReceiveAsType(String exchange, String routingKey, Object message, + default @Nullable T convertSendAndReceiveAsType(String exchange, String routingKey, Object message, @Nullable CorrelationData correlationData, ParameterizedTypeReference responseType) - throws AmqpException { + throws AmqpException { return convertSendAndReceiveAsType(exchange, routingKey, message, null, correlationData, responseType); } @@ -387,9 +382,8 @@ default T convertSendAndReceiveAsType(String exchange, String routingKey, Ob * @return the response if there is one * @throws AmqpException if there is a problem */ - @Nullable - T convertSendAndReceiveAsType(Object message, MessagePostProcessor messagePostProcessor, - CorrelationData correlationData, ParameterizedTypeReference responseType) throws AmqpException; + @Nullable T convertSendAndReceiveAsType(Object message, @Nullable MessagePostProcessor messagePostProcessor, + @Nullable CorrelationData correlationData, ParameterizedTypeReference responseType) throws AmqpException; /** * Basic RPC pattern with conversion. Send a Java object converted to a message to a @@ -408,9 +402,8 @@ T convertSendAndReceiveAsType(Object message, MessagePostProcessor messagePo * @return the response if there is one * @throws AmqpException if there is a problem */ - @Nullable - T convertSendAndReceiveAsType(String routingKey, Object message, - MessagePostProcessor messagePostProcessor, CorrelationData correlationData, + @Nullable T convertSendAndReceiveAsType(String routingKey, Object message, + @Nullable MessagePostProcessor messagePostProcessor, @Nullable CorrelationData correlationData, ParameterizedTypeReference responseType) throws AmqpException; /** @@ -431,13 +424,11 @@ T convertSendAndReceiveAsType(String routingKey, Object message, * @return the response if there is one * @throws AmqpException if there is a problem */ - @Nullable - T convertSendAndReceiveAsType(String exchange, String routingKey, Object message, + @Nullable T convertSendAndReceiveAsType(String exchange, String routingKey, Object message, @Nullable MessagePostProcessor messagePostProcessor, @Nullable CorrelationData correlationData, ParameterizedTypeReference responseType) throws AmqpException; - @Override default void start() { // No-op - implemented for backward compatibility diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java index de3af0c8a6..dc319b3a08 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java @@ -52,11 +52,11 @@ import com.rabbitmq.client.ShutdownSignalException; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpConnectException; import org.springframework.amqp.AmqpException; import org.springframework.amqp.AmqpIOException; -import org.springframework.amqp.AmqpIllegalStateException; import org.springframework.amqp.AmqpRejectAndDontRequeueException; import org.springframework.amqp.core.Address; import org.springframework.amqp.core.AmqpMessageReturnedException; @@ -112,7 +112,6 @@ import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.lang.Nullable; import org.springframework.retry.RecoveryCallback; import org.springframework.retry.RetryCallback; import org.springframework.retry.support.RetryTemplate; @@ -187,7 +186,7 @@ public class RabbitTemplate extends RabbitAccessor // NOSONAR type line count /* * Not static as normal since we want this TL to be scoped within the template instance. */ - private final ThreadLocal dedicatedChannels = new ThreadLocal<>(); + private final ThreadLocal<@Nullable Channel> dedicatedChannels = new ThreadLocal<>(); private final AtomicInteger activeTemplateCallbacks = new AtomicInteger(); @@ -215,6 +214,7 @@ public class RabbitTemplate extends RabbitAccessor // NOSONAR type line count private final Map consumerArgs = new HashMap<>(); + @SuppressWarnings("NullAway.Init") private ApplicationContext applicationContext; private String exchange = DEFAULT_EXCHANGE; @@ -222,7 +222,7 @@ public class RabbitTemplate extends RabbitAccessor // NOSONAR type line count private String routingKey = DEFAULT_ROUTING_KEY; // The default queue name that will be used for synchronous receives. - private String defaultReceiveQueue; + private @Nullable String defaultReceiveQueue; private long receiveTimeout = 0; @@ -234,40 +234,39 @@ public class RabbitTemplate extends RabbitAccessor // NOSONAR type line count private String encoding = DEFAULT_ENCODING; - private String replyAddress; + private @Nullable String replyAddress; - @Nullable - private ConfirmCallback confirmCallback; + private @Nullable ConfirmCallback confirmCallback; - private ReturnsCallback returnsCallback; + private @Nullable ReturnsCallback returnsCallback; private Expression mandatoryExpression = new ValueExpression<>(false); - private String correlationKey = null; + private @Nullable String correlationKey = null; - private RetryTemplate retryTemplate; + private @Nullable RetryTemplate retryTemplate; - private RecoveryCallback recoveryCallback; + private @Nullable RecoveryCallback recoveryCallback; - private Expression sendConnectionFactorySelectorExpression; + private @Nullable Expression sendConnectionFactorySelectorExpression; - private Expression receiveConnectionFactorySelectorExpression; + private @Nullable Expression receiveConnectionFactorySelectorExpression; private boolean useDirectReplyToContainer = true; private boolean useTemporaryReplyQueues; - private Collection beforePublishPostProcessors; + private @Nullable Collection beforePublishPostProcessors; - private Collection afterReceivePostProcessors; + private @Nullable Collection afterReceivePostProcessors; - private CorrelationDataPostProcessor correlationDataPostProcessor; + private @Nullable CorrelationDataPostProcessor correlationDataPostProcessor; - private Expression userIdExpression; + private @Nullable Expression userIdExpression; private String beanName = "rabbitTemplate"; - private Executor taskExecutor; + private @Nullable Executor taskExecutor; private boolean userCorrelationId; @@ -275,14 +274,13 @@ public class RabbitTemplate extends RabbitAccessor // NOSONAR type line count private boolean noLocalReplyConsumer; - private ErrorHandler replyErrorHandler; + private @Nullable ErrorHandler replyErrorHandler; private boolean useChannelForCorrelation; private boolean observationEnabled; - @Nullable - private RabbitTemplateObservationConvention observationConvention; + private @Nullable RabbitTemplateObservationConvention observationConvention; private volatile boolean usingFastReplyTo; @@ -295,8 +293,9 @@ public class RabbitTemplate extends RabbitAccessor // NOSONAR type line count /** * Convenient constructor for use with setter injection. Don't forget to set the connection factory. */ + @SuppressWarnings("this-escape") public RabbitTemplate() { - initDefaultStrategies(); // NOSONAR - intentionally overridable; other assertions will check + initDefaultStrategies(); } /** @@ -395,8 +394,7 @@ public void setDefaultReceiveQueue(String queue) { * @return the queue or null if not configured. * @since 2.2.22 */ - @Nullable - public String getDefaultReceiveQueue() { + public @Nullable String getDefaultReceiveQueue() { return this.defaultReceiveQueue; } @@ -638,8 +636,7 @@ public void setBeanFactory(BeanFactory beanFactory) throws BeansException { * @return configured before post {@link MessagePostProcessor}s or {@code null}. * @since 3.2 */ - @Nullable - public Collection getBeforePublishPostProcessors() { + public @Nullable Collection getBeforePublishPostProcessors() { return this.beforePublishPostProcessors != null ? Collections.unmodifiableCollection(this.beforePublishPostProcessors) : null; @@ -717,8 +714,7 @@ public void setAfterReceivePostProcessors(MessagePostProcessor... afterReceivePo * @return configured after receive {@link MessagePostProcessor}s or {@code null}. * @since 2.1.5 */ - @Nullable - public Collection getAfterReceivePostProcessors() { + public @Nullable Collection getAfterReceivePostProcessors() { return this.afterReceivePostProcessors != null ? Collections.unmodifiableCollection(this.afterReceivePostProcessors) : null; @@ -911,8 +907,7 @@ public void setUseChannelForCorrelation(boolean useChannelForCorrelation) { * @since 1.5 */ @Override - @Nullable - public Collection expectedQueueNames() { + public @Nullable Collection expectedQueueNames() { this.isListener = true; Collection replyQueue = null; if (this.replyAddress == null || this.replyAddress.equals(Address.AMQ_RABBITMQ_REPLY_TO)) { @@ -920,7 +915,7 @@ public Collection expectedQueueNames() { } else { Address address = new Address(this.replyAddress); - if ("".equals(address.getExchangeName())) { + if (address.getExchangeName().isEmpty()) { replyQueue = Collections.singletonList(address.getRoutingKey()); } else { @@ -939,14 +934,16 @@ public Collection expectedQueueNames() { * @return the collection of correlation data for which confirms have * not been received or null if no such confirms exist. */ - @Nullable - public Collection getUnconfirmed(long age) { + public @Nullable Collection getUnconfirmed(long age) { Set unconfirmed = new HashSet<>(); long cutoffTime = System.currentTimeMillis() - age; for (Channel channel : this.publisherConfirmChannels.keySet()) { Collection confirms = ((PublisherCallbackChannel) channel).expire(this, cutoffTime); for (PendingConfirm confirm : confirms) { - unconfirmed.add(confirm.getCorrelationData()); + CorrelationData correlationData = confirm.getCorrelationData(); + if (correlationData != null) { + unconfirmed.add(correlationData); + } } } return !unconfirmed.isEmpty() ? unconfirmed : null; @@ -1063,6 +1060,7 @@ private void evaluateFastReplyTo() { * This method is invoked once only - when the first message is sent, from a locked block. * @return true to use direct reply-to. */ + @SuppressWarnings("NullAway") // Dataflow analysis limitation protected boolean useDirectReplyTo() { if (this.useTemporaryReplyQueues) { if (this.replyAddress != null) { @@ -1074,7 +1072,7 @@ protected boolean useDirectReplyTo() { } if (this.replyAddress == null || Address.AMQ_RABBITMQ_REPLY_TO.equals(this.replyAddress)) { try { - return execute(channel -> { // NOSONAR - never null + return execute(channel -> { channel.queueDeclarePassive(Address.AMQ_RABBITMQ_REPLY_TO); return true; }); @@ -1102,7 +1100,7 @@ private boolean shouldRethrow(AmqpException ex) { return false; } if (logger.isDebugEnabled()) { - logger.debug("IO error, deferring directReplyTo detection: " + ex.toString()); + logger.debug("IO error, deferring directReplyTo detection: " + ex); } return true; } @@ -1128,8 +1126,8 @@ public void send(final String exchange, final String routingKey, final Message m } @Override - public void send(final String exchange, final String routingKey, - final Message message, @Nullable final CorrelationData correlationData) + public void send(@Nullable String exchange, @Nullable String routingKey, + final Message message, @Nullable CorrelationData correlationData) throws AmqpException { execute(channel -> { @@ -1248,14 +1246,12 @@ public void convertAndSend(String exchange, String routingKey, final Object mess } @Override - @Nullable - public Message receive() throws AmqpException { + public @Nullable Message receive() throws AmqpException { return receive(getRequiredQueue()); } @Override - @Nullable - public Message receive(String queueName) { + public @Nullable Message receive(String queueName) { if (this.receiveTimeout == 0) { return doReceiveNoWait(queueName); } @@ -1270,8 +1266,7 @@ public Message receive(String queueName) { * @return The message, or null if none immediately available. * @since 1.5 */ - @Nullable - protected Message doReceiveNoWait(final String queueName) { + protected @Nullable Message doReceiveNoWait(final String queueName) { Message message = execute(channel -> { GetResponse response = channel.basicGet(queueName, !isChannelTransacted()); // Response can be null is the case that there is no message on the queue. @@ -1296,8 +1291,7 @@ else if (isChannelTransacted()) { } @Override - @Nullable - public Message receive(long timeoutMillis) throws AmqpException { + public @Nullable Message receive(long timeoutMillis) throws AmqpException { String queue = getRequiredQueue(); if (timeoutMillis == 0) { return doReceiveNoWait(queue); @@ -1308,8 +1302,7 @@ public Message receive(long timeoutMillis) throws AmqpException { } @Override - @Nullable - public Message receive(final String queueName, final long timeoutMillis) { + public @Nullable Message receive(final String queueName, final long timeoutMillis) { Message message = execute(channel -> { Delivery delivery = consumeDelivery(channel, queueName, timeoutMillis); if (delivery == null) { @@ -1335,55 +1328,51 @@ else if (isChannelTransacted()) { } @Override - @Nullable - public Object receiveAndConvert() throws AmqpException { + public @Nullable Object receiveAndConvert() throws AmqpException { return receiveAndConvert(getRequiredQueue()); } @Override - @Nullable - public Object receiveAndConvert(String queueName) throws AmqpException { + public @Nullable Object receiveAndConvert(String queueName) throws AmqpException { return receiveAndConvert(queueName, this.receiveTimeout); } @Override - @Nullable - public Object receiveAndConvert(long timeoutMillis) throws AmqpException { + public @Nullable Object receiveAndConvert(long timeoutMillis) throws AmqpException { return receiveAndConvert(getRequiredQueue(), timeoutMillis); } @Override - @Nullable - public Object receiveAndConvert(String queueName, long timeoutMillis) throws AmqpException { + public @Nullable Object receiveAndConvert(String queueName, long timeoutMillis) throws AmqpException { Message response = timeoutMillis == 0 ? doReceiveNoWait(queueName) : receive(queueName, timeoutMillis); if (response != null) { - return getRequiredMessageConverter().fromMessage(response); + return getMessageConverter().fromMessage(response); } return null; } @Override - @Nullable - public T receiveAndConvert(ParameterizedTypeReference type) throws AmqpException { + public @Nullable T receiveAndConvert(ParameterizedTypeReference type) throws AmqpException { return receiveAndConvert(getRequiredQueue(), type); } @Override - @Nullable - public T receiveAndConvert(String queueName, ParameterizedTypeReference type) throws AmqpException { + public @Nullable T receiveAndConvert(String queueName, ParameterizedTypeReference type) + throws AmqpException { + return receiveAndConvert(queueName, this.receiveTimeout, type); } @Override - @Nullable - public T receiveAndConvert(long timeoutMillis, ParameterizedTypeReference type) throws AmqpException { + public @Nullable T receiveAndConvert(long timeoutMillis, ParameterizedTypeReference type) + throws AmqpException { + return receiveAndConvert(getRequiredQueue(), timeoutMillis, type); } @Override @SuppressWarnings(UNCHECKED) - @Nullable - public T receiveAndConvert(String queueName, long timeoutMillis, ParameterizedTypeReference type) + public @Nullable T receiveAndConvert(String queueName, long timeoutMillis, ParameterizedTypeReference type) throws AmqpException { Message response = timeoutMillis == 0 ? doReceiveNoWait(queueName) : receive(queueName, timeoutMillis); @@ -1426,7 +1415,7 @@ public boolean receiveAndReply(final String queueName, ReceiveAndReplyCal public boolean receiveAndReply(ReceiveAndReplyCallback callback, ReplyToAddressCallback replyToAddressCallback) throws AmqpException { - return receiveAndReply(this.getRequiredQueue(), callback, replyToAddressCallback); + return receiveAndReply(getRequiredQueue(), callback, replyToAddressCallback); } @Override @@ -1490,8 +1479,7 @@ else if (channelTransacted) { return receiveMessage; } - @Nullable // NOSONAR complexity - private Delivery consumeDelivery(Channel channel, String queueName, long timeoutMillis) + private @Nullable Delivery consumeDelivery(Channel channel, String queueName, long timeoutMillis) throws IOException { Delivery delivery = null; @@ -1561,7 +1549,7 @@ private boolean sendReply(final ReceiveAndReplyCallback callback, Object receive = receiveMessage; if (!(ReceiveAndReplyMessageCallback.class.isAssignableFrom(callback.getClass()))) { - receive = getRequiredMessageConverter().fromMessage(receiveMessage); + receive = getMessageConverter().fromMessage(receiveMessage); } S reply; @@ -1613,7 +1601,9 @@ private void doSendReply(final ReplyToAddressCallback replyToAddressCallb correlation = messageId; } } - replyMessageProperties.setCorrelationId((String) correlation); + if (correlation != null) { + replyMessageProperties.setCorrelationId((String) correlation); + } } else { replyMessageProperties.setHeader(this.correlationKey, correlation); @@ -1629,117 +1619,101 @@ private void doSendReply(final ReplyToAddressCallback replyToAddressCallb } @Override - @Nullable - public Message sendAndReceive(final Message message) throws AmqpException { + public @Nullable Message sendAndReceive(final Message message) throws AmqpException { return sendAndReceive(message, null); } - @Nullable - public Message sendAndReceive(final Message message, @Nullable CorrelationData correlationData) + public @Nullable Message sendAndReceive(final Message message, @Nullable CorrelationData correlationData) throws AmqpException { return doSendAndReceive(this.exchange, this.routingKey, message, correlationData); } @Override - @Nullable - public Message sendAndReceive(final String routingKey, final Message message) throws AmqpException { + public @Nullable Message sendAndReceive(final String routingKey, final Message message) throws AmqpException { return sendAndReceive(routingKey, message, null); } - @Nullable - public Message sendAndReceive(final String routingKey, final Message message, + public @Nullable Message sendAndReceive(final String routingKey, final Message message, @Nullable CorrelationData correlationData) throws AmqpException { return doSendAndReceive(this.exchange, routingKey, message, correlationData); } @Override - @Nullable - public Message sendAndReceive(final String exchange, final String routingKey, final Message message) + public @Nullable Message sendAndReceive(final String exchange, final String routingKey, final Message message) throws AmqpException { return sendAndReceive(exchange, routingKey, message, null); } - @Nullable - public Message sendAndReceive(final String exchange, final String routingKey, final Message message, + public @Nullable Message sendAndReceive(final String exchange, final String routingKey, final Message message, @Nullable CorrelationData correlationData) throws AmqpException { return doSendAndReceive(exchange, routingKey, message, correlationData); } @Override - @Nullable - public Object convertSendAndReceive(final Object message) throws AmqpException { + public @Nullable Object convertSendAndReceive(final Object message) throws AmqpException { return convertSendAndReceive(message, (CorrelationData) null); } @Override - @Nullable - public Object convertSendAndReceive(final Object message, @Nullable CorrelationData correlationData) + public @Nullable Object convertSendAndReceive(final Object message, @Nullable CorrelationData correlationData) throws AmqpException { return convertSendAndReceive(this.exchange, this.routingKey, message, null, correlationData); } @Override - @Nullable - public Object convertSendAndReceive(final String routingKey, final Object message) throws AmqpException { + public @Nullable Object convertSendAndReceive(final String routingKey, final Object message) throws AmqpException { return convertSendAndReceive(routingKey, message, (CorrelationData) null); } @Override - @Nullable - public Object convertSendAndReceive(final String routingKey, final Object message, + public @Nullable Object convertSendAndReceive(final String routingKey, final Object message, @Nullable CorrelationData correlationData) throws AmqpException { return convertSendAndReceive(this.exchange, routingKey, message, null, correlationData); } @Override - @Nullable - public Object convertSendAndReceive(final String exchange, final String routingKey, final Object message) + public @Nullable Object convertSendAndReceive(final String exchange, final String routingKey, final Object message) throws AmqpException { return convertSendAndReceive(exchange, routingKey, message, (CorrelationData) null); } @Override - @Nullable - public Object convertSendAndReceive(final String exchange, final String routingKey, final Object message, + public @Nullable Object convertSendAndReceive(final String exchange, final String routingKey, final Object message, @Nullable CorrelationData correlationData) throws AmqpException { return convertSendAndReceive(exchange, routingKey, message, null, correlationData); } @Override - @Nullable - public Object convertSendAndReceive(final Object message, final MessagePostProcessor messagePostProcessor) + public @Nullable Object convertSendAndReceive(final Object message, final MessagePostProcessor messagePostProcessor) throws AmqpException { return convertSendAndReceive(message, messagePostProcessor, null); } @Override - @Nullable - public Object convertSendAndReceive(final Object message, final MessagePostProcessor messagePostProcessor, + public @Nullable Object convertSendAndReceive(final Object message, final MessagePostProcessor messagePostProcessor, @Nullable CorrelationData correlationData) throws AmqpException { return convertSendAndReceive(this.exchange, this.routingKey, message, messagePostProcessor, correlationData); } @Override - @Nullable - public Object convertSendAndReceive(final String routingKey, final Object message, + public @Nullable Object convertSendAndReceive(final String routingKey, final Object message, final MessagePostProcessor messagePostProcessor) throws AmqpException { return convertSendAndReceive(routingKey, message, messagePostProcessor, null); } @Override - @Nullable - public Object convertSendAndReceive(final String routingKey, final Object message, + public @Nullable Object convertSendAndReceive(final String routingKey, final Object message, final MessagePostProcessor messagePostProcessor, @Nullable CorrelationData correlationData) throws AmqpException { @@ -1747,38 +1721,34 @@ public Object convertSendAndReceive(final String routingKey, final Object messag } @Override - @Nullable - public Object convertSendAndReceive(final String exchange, final String routingKey, final Object message, + public @Nullable Object convertSendAndReceive(final String exchange, final String routingKey, final Object message, final MessagePostProcessor messagePostProcessor) throws AmqpException { return convertSendAndReceive(exchange, routingKey, message, messagePostProcessor, null); } @Override - @Nullable - public Object convertSendAndReceive(final String exchange, final String routingKey, final Object message, - @Nullable final MessagePostProcessor messagePostProcessor, - @Nullable final CorrelationData correlationData) throws AmqpException { + public @Nullable Object convertSendAndReceive(String exchange, String routingKey, Object message, + @Nullable MessagePostProcessor messagePostProcessor, + @Nullable CorrelationData correlationData) throws AmqpException { Message replyMessage = convertSendAndReceiveRaw(exchange, routingKey, message, messagePostProcessor, correlationData); if (replyMessage == null) { return null; } - return this.getRequiredMessageConverter().fromMessage(replyMessage); + return getMessageConverter().fromMessage(replyMessage); } @Override - @Nullable - public T convertSendAndReceiveAsType(final Object message, ParameterizedTypeReference responseType) + public @Nullable T convertSendAndReceiveAsType(final Object message, ParameterizedTypeReference responseType) throws AmqpException { return convertSendAndReceiveAsType(message, (CorrelationData) null, responseType); } @Override - @Nullable - public T convertSendAndReceiveAsType(final Object message, @Nullable CorrelationData correlationData, + public @Nullable T convertSendAndReceiveAsType(final Object message, @Nullable CorrelationData correlationData, ParameterizedTypeReference responseType) throws AmqpException { return convertSendAndReceiveAsType(this.exchange, this.routingKey, message, null, correlationData, @@ -1786,16 +1756,14 @@ public T convertSendAndReceiveAsType(final Object message, @Nullable Correla } @Override - @Nullable - public T convertSendAndReceiveAsType(final String routingKey, final Object message, + public @Nullable T convertSendAndReceiveAsType(final String routingKey, final Object message, ParameterizedTypeReference responseType) throws AmqpException { return convertSendAndReceiveAsType(routingKey, message, (CorrelationData) null, responseType); } @Override - @Nullable - public T convertSendAndReceiveAsType(final String routingKey, final Object message, + public @Nullable T convertSendAndReceiveAsType(final String routingKey, final Object message, @Nullable CorrelationData correlationData, ParameterizedTypeReference responseType) throws AmqpException { @@ -1803,16 +1771,14 @@ public T convertSendAndReceiveAsType(final String routingKey, final Object m } @Override - @Nullable - public T convertSendAndReceiveAsType(final String exchange, final String routingKey, final Object message, + public @Nullable T convertSendAndReceiveAsType(final String exchange, final String routingKey, Object message, ParameterizedTypeReference responseType) throws AmqpException { return convertSendAndReceiveAsType(exchange, routingKey, message, (CorrelationData) null, responseType); } @Override - @Nullable - public T convertSendAndReceiveAsType(final Object message, + public @Nullable T convertSendAndReceiveAsType(final Object message, @Nullable final MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) throws AmqpException { @@ -1820,9 +1786,8 @@ public T convertSendAndReceiveAsType(final Object message, } @Override - @Nullable - public T convertSendAndReceiveAsType(final Object message, - @Nullable final MessagePostProcessor messagePostProcessor, + public @Nullable T convertSendAndReceiveAsType(final Object message, + @Nullable MessagePostProcessor messagePostProcessor, @Nullable CorrelationData correlationData, ParameterizedTypeReference responseType) throws AmqpException { @@ -1831,8 +1796,7 @@ public T convertSendAndReceiveAsType(final Object message, } @Override - @Nullable - public T convertSendAndReceiveAsType(final String routingKey, final Object message, + public @Nullable T convertSendAndReceiveAsType(final String routingKey, final Object message, @Nullable final MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) throws AmqpException { @@ -1840,9 +1804,8 @@ public T convertSendAndReceiveAsType(final String routingKey, final Object m } @Override - @Nullable - public T convertSendAndReceiveAsType(final String routingKey, final Object message, - @Nullable final MessagePostProcessor messagePostProcessor, @Nullable CorrelationData correlationData, + public @Nullable T convertSendAndReceiveAsType(final String routingKey, final Object message, + @Nullable MessagePostProcessor messagePostProcessor, @Nullable CorrelationData correlationData, ParameterizedTypeReference responseType) throws AmqpException { return convertSendAndReceiveAsType(this.exchange, routingKey, message, messagePostProcessor, correlationData, @@ -1850,8 +1813,7 @@ public T convertSendAndReceiveAsType(final String routingKey, final Object m } @Override - @Nullable - public T convertSendAndReceiveAsType(final String exchange, final String routingKey, final Object message, + public @Nullable T convertSendAndReceiveAsType(final String exchange, final String routingKey, Object message, final MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) throws AmqpException { @@ -1860,10 +1822,9 @@ public T convertSendAndReceiveAsType(final String exchange, final String rou @Override @SuppressWarnings(UNCHECKED) - @Nullable - public T convertSendAndReceiveAsType(final String exchange, final String routingKey, final Object message, - @Nullable final MessagePostProcessor messagePostProcessor, @Nullable final CorrelationData correlationData, - ParameterizedTypeReference responseType) throws AmqpException { + public @Nullable T convertSendAndReceiveAsType(@Nullable String exchange, @Nullable String routingKey, + Object message, @Nullable MessagePostProcessor messagePostProcessor, + @Nullable CorrelationData correlationData, ParameterizedTypeReference responseType) throws AmqpException { Message replyMessage = convertSendAndReceiveRaw(exchange, routingKey, message, messagePostProcessor, correlationData); @@ -1885,10 +1846,9 @@ public T convertSendAndReceiveAsType(final String exchange, final String rou * @return the reply message or null if a timeout occurs. * @since 1.6.6 */ - @Nullable - protected Message convertSendAndReceiveRaw(final String exchange, final String routingKey, final Object message, - @Nullable final MessagePostProcessor messagePostProcessor, - @Nullable final CorrelationData correlationData) { + protected @Nullable Message convertSendAndReceiveRaw(@Nullable String exchange, @Nullable String routingKey, + Object message, @Nullable MessagePostProcessor messagePostProcessor, + @Nullable CorrelationData correlationData) { Message requestMessage = convertMessageIfNecessary(message); if (messagePostProcessor != null) { @@ -1902,7 +1862,7 @@ protected Message convertMessageIfNecessary(final Object object) { if (object instanceof Message msg) { return msg; } - return getRequiredMessageConverter().toMessage(object, new MessageProperties()); + return getMessageConverter().toMessage(object, new MessageProperties()); } /** @@ -1914,9 +1874,8 @@ protected Message convertMessageIfNecessary(final Object object) { * @param correlationData the correlation data for confirms * @return the message that is received in reply */ - @Nullable - protected Message doSendAndReceive(final String exchange, final String routingKey, final Message message, - @Nullable CorrelationData correlationData) { + protected @Nullable Message doSendAndReceive(@Nullable String exchange, @Nullable String routingKey, + Message message, @Nullable CorrelationData correlationData) { if (!this.evaluatedFastReplyTo) { this.fastReplyToLock.lock(); @@ -1941,9 +1900,8 @@ else if (this.replyAddress == null || this.usingFastReplyTo) { } } - @Nullable - protected Message doSendAndReceiveWithTemporary(final String exchange, final String routingKey, - final Message message, @Nullable final CorrelationData correlationData) { + protected @Nullable Message doSendAndReceiveWithTemporary(@Nullable String exchange, @Nullable String routingKey, + final Message message, @Nullable CorrelationData correlationData) { return execute(channel -> { final PendingReply pendingReply = new PendingReply(); @@ -2017,20 +1975,18 @@ private void cancelConsumerQuietly(Channel channel, DefaultConsumer consumer) { RabbitUtils.cancel(channel, consumer.getConsumerTag()); } - @Nullable - protected Message doSendAndReceiveWithFixed(final String exchange, final String routingKey, final Message message, - @Nullable final CorrelationData correlationData) { + protected @Nullable Message doSendAndReceiveWithFixed(@Nullable String exchange, @Nullable String routingKey, + Message message, @Nullable CorrelationData correlationData) { Assert.state(this.isListener, () -> "RabbitTemplate is not configured as MessageListener - " + "cannot use a 'replyAddress': " + this.replyAddress); - return execute(channel -> { - return doSendAndReceiveAsListener(exchange, routingKey, message, correlationData, channel, false); - }, obtainTargetConnectionFactory(this.sendConnectionFactorySelectorExpression, message)); + return execute(channel -> + doSendAndReceiveAsListener(exchange, routingKey, message, correlationData, channel, false), + obtainTargetConnectionFactory(this.sendConnectionFactorySelectorExpression, message)); } - @Nullable - private Message doSendAndReceiveWithDirect(String exchange, String routingKey, Message message, - @Nullable CorrelationData correlationData) { + private @Nullable Message doSendAndReceiveWithDirect(@Nullable String exchange, @Nullable String routingKey, + Message message, @Nullable CorrelationData correlationData) { ConnectionFactory connectionFactory = obtainTargetConnectionFactory( this.sendConnectionFactorySelectorExpression, message); @@ -2093,9 +2049,9 @@ private DirectReplyToMessageListenerContainer createReplyToContainer(ConnectionF return container; } - @Nullable - private Message doSendAndReceiveAsListener(final String exchange, final String routingKey, final Message message, - @Nullable final CorrelationData correlationData, Channel channel, boolean noCorrelation) throws Exception { // NOSONAR + private @Nullable Message doSendAndReceiveAsListener(@Nullable String exchange, @Nullable String routingKey, + Message message, @Nullable CorrelationData correlationData, Channel channel, boolean noCorrelation) + throws Exception { // NOSONAR final PendingReply pendingReply = new PendingReply(); String messageTag = null; @@ -2119,7 +2075,7 @@ private Message doSendAndReceiveAsListener(final String exchange, final String r if (logger.isDebugEnabled()) { logger.debug("Sending message with tag " + messageTag); } - Message reply = null; + Message reply; try { reply = exchangeMessages(exchange, routingKey, message, correlationData, channel, pendingReply, messageTag); @@ -2170,9 +2126,8 @@ private void saveAndSetProperties(final Message message, final PendingReply pend } } - @Nullable - private Message exchangeMessages(final String exchange, final String routingKey, final Message message, - @Nullable final CorrelationData correlationData, Channel channel, final PendingReply pendingReply, + private @Nullable Message exchangeMessages(@Nullable String exchange, @Nullable String routingKey, Message message, + @Nullable CorrelationData correlationData, Channel channel, final PendingReply pendingReply, String messageTag) throws InterruptedException { Message reply; @@ -2194,7 +2149,7 @@ private Message exchangeMessages(final String exchange, final String routingKey, * @param correlationId the correlationId * @since 2.1.2 */ - protected void replyTimedOut(String correlationId) { + protected void replyTimedOut(@Nullable String correlationId) { // NOSONAR } @@ -2210,14 +2165,12 @@ public Boolean isMandatoryFor(final Message message) { } @Override - @Nullable - public T execute(ChannelCallback action) { + public @Nullable T execute(ChannelCallback action) { return execute(action, getConnectionFactory()); } @SuppressWarnings(UNCHECKED) - @Nullable - private T execute(final ChannelCallback action, final ConnectionFactory connectionFactory) { + private @Nullable T execute(final ChannelCallback action, final ConnectionFactory connectionFactory) { if (this.retryTemplate != null) { try { return this.retryTemplate.execute( @@ -2236,8 +2189,7 @@ private T execute(final ChannelCallback action, final ConnectionFactory c } } - @Nullable - private T doExecute(ChannelCallback action, ConnectionFactory connectionFactory) { // NOSONAR complexity + private @Nullable T doExecute(ChannelCallback action, ConnectionFactory connectionFactory) { // NOSONAR complexity Assert.notNull(action, "Callback object must not be null"); Channel channel = null; boolean invokeScope = false; @@ -2261,14 +2213,8 @@ private T doExecute(ChannelCallback action, ConnectionFactory connectionF else { connection = ConnectionFactoryUtils.createConnection(connectionFactory, this.usePublisherConnection); // NOSONAR - RabbitUtils closes - if (connection == null) { - throw new IllegalStateException("Connection factory returned a null connection"); - } try { channel = connection.createChannel(false); - if (channel == null) { - throw new IllegalStateException("Connection returned a null channel"); - } } catch (RuntimeException e) { RabbitUtils.closeConnection(connection); @@ -2283,7 +2229,7 @@ private T doExecute(ChannelCallback action, ConnectionFactory connectionF return invokeAction(action, connectionFactory, channel); } catch (Exception ex) { - if (isChannelLocallyTransacted(channel)) { + if (isChannelLocallyTransacted(channel) && resourceHolder != null) { resourceHolder.rollbackAll(); } throw convertRabbitAccessException(ex); @@ -2307,8 +2253,7 @@ private void cleanUpAfterAction(@Nullable Channel channel, boolean invokeScope, } } - @Nullable - private T invokeAction(ChannelCallback action, ConnectionFactory connectionFactory, Channel channel) + private @Nullable T invokeAction(ChannelCallback action, ConnectionFactory connectionFactory, Channel channel) throws Exception { // NOSONAR see the callback if (isPublisherConfirmsOrReturns(connectionFactory)) { @@ -2323,9 +2268,8 @@ private T invokeAction(ChannelCallback action, ConnectionFactory connecti } @Override - @Nullable - public T invoke(OperationsCallback action, @Nullable com.rabbitmq.client.ConfirmCallback acks, - @Nullable com.rabbitmq.client.ConfirmCallback nacks) { + public @Nullable T invoke(OperationsCallback action, com.rabbitmq.client.@Nullable ConfirmCallback acks, + com.rabbitmq.client.@Nullable ConfirmCallback nacks) { final Channel currentChannel = this.dedicatedChannels.get(); Assert.state(currentChannel == null, () -> "Nested invoke() calls are not supported; channel '" + currentChannel @@ -2348,15 +2292,9 @@ public T invoke(OperationsCallback action, @Nullable com.rabbitmq.client. if (this.usePublisherConnection && connectionFactory.getPublisherConnectionFactory() != null) { connectionFactory = connectionFactory.getPublisherConnectionFactory(); } - connection = connectionFactory.createConnection(); // NOSONAR - RabbitUtils - if (connection == null) { - throw new IllegalStateException("Connection factory returned a null connection"); - } + connection = connectionFactory.createConnection(); try { channel = connection.createChannel(false); - if (channel == null) { - throw new IllegalStateException("Connection returned a null channel"); - } if (!connectionFactory.isPublisherConfirms() && !connectionFactory.isSimplePublisherConfirms()) { RabbitUtils.setPhysicalCloseRequired(channel, true); } @@ -2376,9 +2314,8 @@ public T invoke(OperationsCallback action, @Nullable com.rabbitmq.client. } } - @Nullable - private ConfirmListener addConfirmListener(@Nullable com.rabbitmq.client.ConfirmCallback acks, - @Nullable com.rabbitmq.client.ConfirmCallback nacks, Channel channel) { + private @Nullable ConfirmListener addConfirmListener(com.rabbitmq.client.@Nullable ConfirmCallback acks, + com.rabbitmq.client.@Nullable ConfirmCallback nacks, Channel channel) { ConfirmListener listener = null; if (acks != null && nacks != null && channel instanceof ChannelProxy proxy @@ -2391,7 +2328,7 @@ private ConfirmListener addConfirmListener(@Nullable com.rabbitmq.client.Confirm private void cleanUpAfterAction(@Nullable RabbitResourceHolder resourceHolder, @Nullable Connection connection, @Nullable Channel channel, @Nullable ConfirmListener listener) { - if (listener != null) { + if (channel != null && listener != null) { channel.removeConfirmListener(listener); } this.activeTemplateCallbacks.decrementAndGet(); @@ -2451,7 +2388,7 @@ private boolean isPublisherConfirmsOrReturns(ConnectionFactory connectionFactory * @param mandatory The mandatory flag. * @param correlationData The correlation data. */ - public void doSend(Channel channel, String exchangeArg, String routingKeyArg, Message message, + public void doSend(Channel channel, @Nullable String exchangeArg, @Nullable String routingKeyArg, Message message, boolean mandatory, @Nullable CorrelationData correlationData) { String exch = nullSafeExchange(exchangeArg); @@ -2499,7 +2436,7 @@ protected void observeTheSend(Channel channel, Message message, boolean mandator ObservationRegistry registry = getObservationRegistry(); Observation observation = RabbitTemplateObservation.TEMPLATE_OBSERVATION.observation(this.observationConvention, DefaultRabbitTemplateObservationConvention.INSTANCE, - () -> new RabbitMessageSenderContext(message, this.beanName, exch, rKey), registry); + () -> new RabbitMessageSenderContext(message, this.beanName, exch, rKey), registry); observation.observe(() -> sendToRabbit(channel, exch, rKey, mandatory, message)); } @@ -2543,11 +2480,13 @@ private void setupConfirm(Channel channel, Message message, @Nullable Correlatio if ((publisherConfirms || this.confirmCallback != null) && channel instanceof PublisherCallbackChannel publisherCallbackChannel) { + long nextPublishSeqNo = channel.getNextPublishSeqNo(); if (nextPublishSeqNo > 0) { - CorrelationData correlationData = this.correlationDataPostProcessor != null - ? this.correlationDataPostProcessor.postProcess(message, correlationDataArg) - : correlationDataArg; + CorrelationData correlationData = + this.correlationDataPostProcessor != null + ? this.correlationDataPostProcessor.postProcess(message, correlationDataArg) + : correlationDataArg; message.getMessageProperties().setPublishSequenceNumber(nextPublishSeqNo); publisherCallbackChannel.addPendingConfirm(this, nextPublishSeqNo, new PendingConfirm(correlationData, System.currentTimeMillis())); @@ -2602,17 +2541,8 @@ private Message buildMessage(Envelope envelope, BasicProperties properties, byte return message; } - private MessageConverter getRequiredMessageConverter() throws IllegalStateException { - MessageConverter converter = getMessageConverter(); - if (converter == null) { - throw new AmqpIllegalStateException( - "No 'messageConverter' specified. Check configuration of RabbitTemplate."); - } - return converter; - } - private SmartMessageConverter getRequiredSmartMessageConverter() throws IllegalStateException { - MessageConverter converter = getRequiredMessageConverter(); + MessageConverter converter = getMessageConverter(); Assert.state(converter instanceof SmartMessageConverter, "template's message converter must be a SmartMessageConverter"); return (SmartMessageConverter) converter; @@ -2620,9 +2550,7 @@ private SmartMessageConverter getRequiredSmartMessageConverter() throws IllegalS private String getRequiredQueue() throws IllegalStateException { String name = this.defaultReceiveQueue; - if (name == null) { - throw new AmqpIllegalStateException("No 'queue' specified. Check configuration of RabbitTemplate."); - } + Assert.state(name != null, "No 'queue' specified. Check configuration of RabbitTemplate."); return name; } @@ -2642,12 +2570,6 @@ private String getRequiredQueue() throws IllegalStateException { private Address getReplyToAddress(Message request) throws AmqpException { Address replyTo = request.getMessageProperties().getReplyToAddress(); if (replyTo == null) { - if (this.exchange == null) { - throw new AmqpException( - "Cannot determine ReplyTo message property value: " - + "Request message does not contain reply-to property, " + - "and no default Exchange was set."); - } replyTo = new Address(this.exchange, this.routingKey); } return replyTo; @@ -2679,8 +2601,7 @@ public void addListener(Channel channel) { @Override public void handleConfirm(PendingConfirm pendingConfirm, boolean ack) { if (this.confirmCallback != null) { - this.confirmCallback - .confirm(pendingConfirm.getCorrelationData(), ack, pendingConfirm.getCause()); // NOSONAR never null + this.confirmCallback.confirm(pendingConfirm.getCorrelationData(), ack, pendingConfirm.getCause()); } } @@ -2835,16 +2756,13 @@ public void handleDelivery(String consumerTag, Envelope envelope, BasicPropertie private static final class PendingReply { - @Nullable - private volatile String savedReplyTo; + private volatile @Nullable String savedReplyTo; - @Nullable - private volatile String savedCorrelation; + private volatile @Nullable String savedCorrelation; private final CompletableFuture future = new CompletableFuture<>(); - @Nullable - public String getSavedReplyTo() { + public @Nullable String getSavedReplyTo() { return this.savedReplyTo; } @@ -2852,8 +2770,7 @@ public void setSavedReplyTo(@Nullable String savedReplyTo) { this.savedReplyTo = savedReplyTo; } - @Nullable - public String getSavedCorrelation() { + public @Nullable String getSavedCorrelation() { return this.savedCorrelation; } @@ -2870,8 +2787,7 @@ public Message get() throws InterruptedException { } } - @Nullable - public Message get(long timeout, TimeUnit unit) throws InterruptedException { + public @Nullable Message get(long timeout, TimeUnit unit) throws InterruptedException { try { return this.future.get(timeout, unit); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/package-info.java index c82cc7b544..8cc9254744 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/package-info.java @@ -1,5 +1,5 @@ /** * Provides core classes for Spring Rabbit. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.core; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index c4c4335e71..ded3641380 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -24,6 +24,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Properties; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; @@ -39,6 +40,7 @@ import org.aopalliance.aop.Advice; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpConnectException; import org.springframework.amqp.AmqpIOException; @@ -84,7 +86,6 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.core.task.SimpleAsyncTaskExecutor; -import org.springframework.lang.Nullable; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.interceptor.DefaultTransactionAttribute; import org.springframework.transaction.interceptor.TransactionAttribute; @@ -144,10 +145,9 @@ public abstract class AbstractMessageListenerContainer extends ObservableListene private long shutdownTimeout = DEFAULT_SHUTDOWN_TIMEOUT; - private ApplicationEventPublisher applicationEventPublisher; + private @Nullable ApplicationEventPublisher applicationEventPublisher; - @Nullable - private PlatformTransactionManager transactionManager; + private @Nullable PlatformTransactionManager transactionManager; private TransactionAttribute transactionAttribute = new DefaultTransactionAttribute(); @@ -159,7 +159,7 @@ public abstract class AbstractMessageListenerContainer extends ObservableListene private MessagePropertiesConverter messagePropertiesConverter = new DefaultMessagePropertiesConverter(); - private AmqpAdmin amqpAdmin; + private @Nullable AmqpAdmin amqpAdmin; private boolean missingQueuesFatal = true; @@ -187,7 +187,7 @@ public abstract class AbstractMessageListenerContainer extends ObservableListene private boolean exposeListenerChannel = true; - private MessageListener messageListener; + private @Nullable MessageListener messageListener; private AcknowledgeMode acknowledgeMode = AcknowledgeMode.AUTO; @@ -195,12 +195,11 @@ public abstract class AbstractMessageListenerContainer extends ObservableListene private boolean initialized; - private Collection afterReceivePostProcessors; + private @Nullable Collection afterReceivePostProcessors; private Advice[] adviceChain = new Advice[0]; - @Nullable - private ConsumerTagStrategy consumerTagStrategy; + private @Nullable ConsumerTagStrategy consumerTagStrategy; private boolean exclusive; @@ -242,10 +241,10 @@ public abstract class AbstractMessageListenerContainer extends ObservableListene private boolean asyncReplies; - private MessageAckListener messageAckListener = (success, deliveryTag, cause) -> { }; + private MessageAckListener messageAckListener = (success, deliveryTag, cause) -> { + }; - @Nullable - private RabbitListenerObservationConvention observationConvention; + private @Nullable RabbitListenerObservationConvention observationConvention; private boolean forceStop; @@ -254,8 +253,7 @@ public void setApplicationEventPublisher(ApplicationEventPublisher applicationEv this.applicationEventPublisher = applicationEventPublisher; } - @Nullable - protected ApplicationEventPublisher getApplicationEventPublisher() { + protected @Nullable ApplicationEventPublisher getApplicationEventPublisher() { return this.applicationEventPublisher; } @@ -457,8 +455,7 @@ protected void checkMessageListener(Object listener) { * @return the message listener. */ @Override - @Nullable - public MessageListener getMessageListener() { + public @Nullable MessageListener getMessageListener() { return this.messageListener; } @@ -589,8 +586,8 @@ public int getPhase() { public ConnectionFactory getConnectionFactory() { ConnectionFactory connectionFactory = super.getConnectionFactory(); if (connectionFactory instanceof RoutingConnectionFactory rcf) { - ConnectionFactory targetConnectionFactory = rcf - .getTargetConnectionFactory(getRoutingLookupKey()); // NOSONAR never null + @SuppressWarnings("NullAway") // Dataflow analysis limitation + ConnectionFactory targetConnectionFactory = rcf.getTargetConnectionFactory(getRoutingLookupKey()); if (targetConnectionFactory != null) { return targetConnectionFactory; } @@ -636,8 +633,7 @@ public void setForceCloseChannel(boolean forceCloseChannel) { * @since 1.6.9 * @see #setLookupKeyQualifier(String) */ - @Nullable - protected String getRoutingLookupKey() { + protected @Nullable String getRoutingLookupKey() { return super.getConnectionFactory() instanceof RoutingConnectionFactory ? this.lookupKeyQualifier + queuesAsListString() : null; @@ -650,14 +646,13 @@ private String queuesAsListString() { } /** - * Return the (@link RoutingConnectionFactory} if the connection factory is a + * Return the {@link RoutingConnectionFactory} if the connection factory is a * {@link RoutingConnectionFactory}; null otherwise. * @return the {@link RoutingConnectionFactory} or null. * @since 1.6.9 */ - @Nullable - protected RoutingConnectionFactory getRoutingConnectionFactory() { - return super.getConnectionFactory() instanceof RoutingConnectionFactory rcf ? rcf : null; + protected @Nullable RoutingConnectionFactory getRoutingConnectionFactory() { + return super.getConnectionFactory() instanceof RoutingConnectionFactory rcf ? rcf : null; } /** @@ -675,8 +670,7 @@ public void setConsumerTagStrategy(ConsumerTagStrategy consumerTagStrategy) { * @return the strategy. * @since 2.0 */ - @Nullable - protected ConsumerTagStrategy getConsumerTagStrategy() { + protected @Nullable ConsumerTagStrategy getConsumerTagStrategy() { return this.consumerTagStrategy; } @@ -843,8 +837,7 @@ public void setTransactionManager(PlatformTransactionManager transactionManager) this.transactionManager = transactionManager; } - @Nullable - protected PlatformTransactionManager getTransactionManager() { + protected @Nullable PlatformTransactionManager getTransactionManager() { return this.transactionManager; } @@ -915,8 +908,7 @@ protected MessagePropertiesConverter getMessagePropertiesConverter() { return this.messagePropertiesConverter; } - @Nullable - protected AmqpAdmin getAmqpAdmin() { + protected @Nullable AmqpAdmin getAmqpAdmin() { return this.amqpAdmin; } @@ -970,7 +962,6 @@ protected boolean isMismatchedQueuesFatal() { return this.mismatchedQueuesFatal; } - public void setPossibleAuthenticationFailureFatal(boolean possibleAuthenticationFailureFatal) { doSetPossibleAuthenticationFailureFatal(possibleAuthenticationFailureFatal); this.possibleAuthenticationFailureFatalSet = true; @@ -987,6 +978,7 @@ public boolean isPossibleAuthenticationFailureFatal() { protected boolean isPossibleAuthenticationFailureFatalSet() { return this.possibleAuthenticationFailureFatalSet; } + protected boolean isAsyncReplies() { return this.asyncReplies; } @@ -1099,7 +1091,7 @@ protected BatchingStrategy getBatchingStrategy() { return this.batchingStrategy; } - protected Collection getAfterReceivePostProcessors() { + protected @Nullable Collection getAfterReceivePostProcessors() { return this.afterReceivePostProcessors; } @@ -1141,12 +1133,25 @@ protected JavaLangErrorHandler getJavaLangErrorHandler() { * is called. * @param javaLangErrorHandler the handler. * @since 2.2.12 + * @deprecated in favor of {@link #setJavaLangErrorHandler(JavaLangErrorHandler)} */ + @Deprecated(since = "4.0.0", forRemoval = true) public void setjavaLangErrorHandler(JavaLangErrorHandler javaLangErrorHandler) { Assert.notNull(javaLangErrorHandler, "'javaLangErrorHandler' cannot be null"); this.javaLangErrorHandler = javaLangErrorHandler; } + /** + * Provide a JavaLangErrorHandler implementation; by default, {@code System.exit(99)} + * is called. + * @param javaLangErrorHandler the handler. + * @since 4.0.0 + */ + public void setJavaLangErrorHandler(JavaLangErrorHandler javaLangErrorHandler) { + Assert.notNull(javaLangErrorHandler, "'javaLangErrorHandler' cannot be null"); + this.javaLangErrorHandler = javaLangErrorHandler; + } + /** * Set a {@link MessageAckListener} to use when ack a message(messages) in * {@link AcknowledgeMode#AUTO} mode. @@ -1368,7 +1373,6 @@ public void stop(Runnable callback) { protected void shutdownAndWaitOrCallback(@Nullable Runnable callback) { } - /** * @return Whether this container is currently active, that is, whether it has been set up but not shut down yet. */ @@ -1469,18 +1473,13 @@ public final boolean isRunning() { * @see #setErrorHandler */ protected void invokeErrorHandler(Throwable ex) { - if (this.errorHandler != null) { - try { - this.errorHandler.handleError(ex); - } - catch (Exception e) { - LogFactory.getLog(this.errorHandlerLoggerName).error( - "Execution of Rabbit message listener failed, and the error handler threw an exception", e); - throw e; - } + try { + this.errorHandler.handleError(ex); } - else { - logger.warn("Execution of Rabbit message listener failed, and no ErrorHandler has been set.", ex); + catch (Exception e) { + LogFactory.getLog(this.errorHandlerLoggerName).error( + "Execution of Rabbit message listener failed, and the error handler threw an exception", e); + throw e; } } @@ -1501,7 +1500,7 @@ protected void executeListener(Channel channel, Object data) { if (data instanceof Message message) { observation = RabbitListenerObservation.LISTENER_OBSERVATION.observation(this.observationConvention, DefaultRabbitListenerObservationConvention.INSTANCE, - () -> new RabbitMessageReceiverContext(message, getListenerId()), registry); + () -> new RabbitMessageReceiverContext(message, getListenerId()), registry); observation.observe(() -> executeListenerAndHandleException(channel, data)); } else { @@ -1525,16 +1524,16 @@ protected void executeListenerAndHandleException(Channel channel, Object data) { } try { doExecuteListener(channel, data); - if (sample != null) { + if (micrometerHolder != null && sample != null) { micrometerHolder.success(sample, data instanceof Message message - ? message.getMessageProperties().getConsumerQueue() + ? Objects.requireNonNull(message.getMessageProperties().getConsumerQueue()) : queuesAsListString()); } } catch (RuntimeException ex) { - if (sample != null) { + if (micrometerHolder != null && sample != null) { micrometerHolder.failure(sample, data instanceof Message message - ? message.getMessageProperties().getConsumerQueue() + ? Objects.requireNonNull(message.getMessageProperties().getConsumerQueue()) : queuesAsListString(), ex.getClass().getSimpleName()); } Message message; @@ -1569,10 +1568,6 @@ private void doExecuteListener(Channel channel, Object data) { if (this.afterReceivePostProcessors != null) { for (MessagePostProcessor processor : this.afterReceivePostProcessors) { message = processor.postProcessMessage(message); - if (message == null) { - throw new ImmediateAcknowledgeAmqpException( - "Message Post Processor returned 'null', discarding message"); - } } } if (this.deBatchingEnabled && this.batchingStrategy.canDebatch(message.getMessageProperties())) { @@ -1598,11 +1593,12 @@ protected void invokeListener(Channel channel, Object data) { * @see #setMessageListener(MessageListener) */ protected void actualInvokeListener(Channel channel, Object data) { - Object listener = getMessageListener(); + MessageListener listener = getMessageListener(); + Assert.notNull(listener, "listener cannot be null"); if (listener instanceof ChannelAwareMessageListener chaml) { doInvokeListener(chaml, channel, data); } - else if (listener instanceof MessageListener msgListener) { // NOSONAR + else { boolean bindChannel = isExposeListenerChannel() && isChannelLocallyTransacted(); if (bindChannel) { RabbitResourceHolder resourceHolder = new RabbitResourceHolder(channel, false); @@ -1610,7 +1606,7 @@ else if (listener instanceof MessageListener msgListener) { // NOSONAR TransactionSynchronizationManager.bindResource(getConnectionFactory(), resourceHolder); } try { - doInvokeListener(msgListener, data); + doInvokeListener(listener, data); } finally { if (bindChannel) { @@ -1619,13 +1615,6 @@ else if (listener instanceof MessageListener msgListener) { // NOSONAR } } } - else if (listener != null) { - throw new FatalListenerExecutionException("Only MessageListener and SessionAwareMessageListener supported: " - + listener); - } - else { - throw new FatalListenerExecutionException("No message listener specified - see property 'messageListener'"); - } } /** @@ -1638,10 +1627,10 @@ else if (listener != null) { * @see ChannelAwareMessageListener * @see #setExposeListenerChannel(boolean) */ - @SuppressWarnings(UNCHECKED) + @SuppressWarnings({UNCHECKED, "NullAway"}) // Dataflow analysis limitation protected void doInvokeListener(ChannelAwareMessageListener listener, Channel channel, Object data) { - Message message = null; + Message message; RabbitResourceHolder resourceHolder = null; Channel channelToUse = channel; boolean boundHere = false; @@ -1686,7 +1675,7 @@ protected void doInvokeListener(ChannelAwareMessageListener listener, Channel ch } } finally { - cleanUpAfterInvoke(resourceHolder, channelToUse, boundHere); // NOSONAR channel not null here + cleanUpAfterInvoke(resourceHolder, channelToUse, boundHere); } } @@ -1725,7 +1714,7 @@ private void cleanUpAfterInvoke(@Nullable RabbitResourceHolder resourceHolder, C */ @SuppressWarnings(UNCHECKED) protected void doInvokeListener(MessageListener listener, Object data) { - Message message = null; + Message message; try { if (data instanceof List) { listener.onMessageBatch((List) data); @@ -1796,7 +1785,7 @@ protected ListenerExecutionFailedException wrapToListenerExecutionFailedExceptio return (ListenerExecutionFailedException) e; } - protected void publishConsumerFailedEvent(String reason, boolean fatal, @Nullable Throwable t) { + protected void publishConsumerFailedEvent(@Nullable String reason, boolean fatal, @Nullable Throwable t) { if (this.applicationEventPublisher != null) { this.applicationEventPublisher .publishEvent(t == null ? new ListenerContainerConsumerTerminatedEvent(this, reason) : @@ -1837,12 +1826,12 @@ protected void configureAdminIfNeeded() { if ((isAutoDeclare() || isMismatchedQueuesFatal()) && this.logger.isDebugEnabled()) { logger.debug("For 'autoDeclare' and 'mismatchedQueuesFatal' to work, there must be exactly one " + "AmqpAdmin in the context or you must inject one into this container; found: " - + admins.size() + " for container " + toString()); + + admins.size() + " for container " + this); } if (isMismatchedQueuesFatal()) { throw new IllegalStateException("When 'mismatchedQueuesFatal' is 'true', there must be exactly " + "one AmqpAdmin in the context or you must inject one into this container; found: " - + admins.size() + " for container " + toString()); + + admins.size() + " for container " + this); } } } @@ -1868,9 +1857,7 @@ protected void checkMismatchedQueues() { else { try { Connection connection = getConnectionFactory().createConnection(); // NOSONAR - if (connection != null) { - connection.close(); - } + connection.close(); } catch (Exception e) { logger.info("Broker not available; cannot force queue declarations during start: " + e.getMessage()); @@ -2120,7 +2107,7 @@ protected WrappedTransactionException(Throwable cause) { public static class DefaultExclusiveConsumerLogger implements ConditionalExceptionLogger { @Override - public void log(Log logger, String message, Throwable cause) { + public void log(Log logger, String message, @Nullable Throwable cause) { if (logger.isDebugEnabled()) { logger.debug(message + ": " + cause); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java index 95bcaffc40..bfa62b4234 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 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. @@ -21,6 +21,8 @@ import java.util.Collection; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.AmqpAdmin; import org.springframework.amqp.core.MessageListener; @@ -37,7 +39,6 @@ import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.core.task.TaskExecutor; import org.springframework.expression.BeanResolver; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -55,7 +56,7 @@ */ public abstract class AbstractRabbitListenerEndpoint implements RabbitListenerEndpoint, BeanFactoryAware { - private String id; + private @Nullable String id; private final Collection queues = new ArrayList<>(); @@ -63,37 +64,37 @@ public abstract class AbstractRabbitListenerEndpoint implements RabbitListenerEn private boolean exclusive; - private Integer priority; + private @Nullable Integer priority; - private String concurrency; + private @Nullable String concurrency; - private AmqpAdmin admin; + private @Nullable AmqpAdmin admin; - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; - private BeanExpressionResolver resolver; + private @Nullable BeanExpressionResolver resolver; - private BeanExpressionContext expressionContext; + private @Nullable BeanExpressionContext expressionContext; - private BeanResolver beanResolver; + private @Nullable BeanResolver beanResolver; - private String group; + private @Nullable String group; - private Boolean autoStartup; + private @Nullable Boolean autoStartup; - private MessageConverter messageConverter; + private @Nullable MessageConverter messageConverter; - private TaskExecutor taskExecutor; + private @Nullable TaskExecutor taskExecutor; - private Boolean batchListener; + private @Nullable Boolean batchListener; - private BatchingStrategy batchingStrategy; + private @Nullable BatchingStrategy batchingStrategy; - private AcknowledgeMode ackMode; + private @Nullable AcknowledgeMode ackMode; - private ReplyPostProcessor replyPostProcessor; + private @Nullable ReplyPostProcessor replyPostProcessor; - private String replyContentType; + private @Nullable String replyContentType; private boolean converterWinsContentType = true; @@ -107,20 +108,19 @@ public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanResolver = new BeanFactoryResolver(beanFactory); } - @Nullable - protected BeanFactory getBeanFactory() { + protected @Nullable BeanFactory getBeanFactory() { return this.beanFactory; } - protected BeanExpressionResolver getResolver() { + protected @Nullable BeanExpressionResolver getResolver() { return this.resolver; } - protected BeanResolver getBeanResolver() { + protected @Nullable BeanResolver getBeanResolver() { return this.beanResolver; } - protected BeanExpressionContext getBeanExpressionContext() { + protected @Nullable BeanExpressionContext getBeanExpressionContext() { return this.expressionContext; } @@ -129,7 +129,7 @@ public void setId(String id) { } @Override - public String getId() { + public @Nullable String getId() { return this.id; } @@ -200,7 +200,7 @@ public void setPriority(Integer priority) { * @return the priority of this endpoint or {@code null} if * no priority is set. */ - public Integer getPriority() { + public @Nullable Integer getPriority() { return this.priority; } @@ -210,7 +210,7 @@ public Integer getPriority() { * @param concurrency the concurrency. * @since 2.0 */ - public void setConcurrency(String concurrency) { + public void setConcurrency(@Nullable String concurrency) { this.concurrency = concurrency; } @@ -221,7 +221,7 @@ public void setConcurrency(String concurrency) { * @since 2.0 */ @Override - public String getConcurrency() { + public @Nullable String getConcurrency() { return this.concurrency; } @@ -237,12 +237,12 @@ public void setAdmin(AmqpAdmin admin) { * @return the {@link AmqpAdmin} instance to use or {@code null} if * none is configured. */ - public AmqpAdmin getAdmin() { + public @Nullable AmqpAdmin getAdmin() { return this.admin; } @Override - public String getGroup() { + public @Nullable String getGroup() { return this.group; } @@ -255,7 +255,6 @@ public void setGroup(String group) { this.group = group; } - /** * Override the default autoStartup property. * @param autoStartup the autoStartup. @@ -266,12 +265,12 @@ public void setAutoStartup(Boolean autoStartup) { } @Override - public Boolean getAutoStartup() { + public @Nullable Boolean getAutoStartup() { return this.autoStartup; } @Override - public MessageConverter getMessageConverter() { + public @Nullable MessageConverter getMessageConverter() { return this.messageConverter; } @@ -281,7 +280,7 @@ public void setMessageConverter(MessageConverter messageConverter) { } @Override - public TaskExecutor getTaskExecutor() { + public @Nullable TaskExecutor getTaskExecutor() { return this.taskExecutor; } @@ -302,14 +301,13 @@ public boolean isBatchListener() { return this.batchListener != null && this.batchListener; } - @Override /** * True if this endpoint is for a batch listener. * @return {@link Boolean#TRUE} if batch. * @since 3.0 */ - @Nullable - public Boolean getBatchListener() { + @Override + public @Nullable Boolean getBatchListener() { return this.batchListener; } @@ -325,8 +323,7 @@ public void setBatchListener(boolean batchListener) { } @Override - @Nullable - public BatchingStrategy getBatchingStrategy() { + public @Nullable BatchingStrategy getBatchingStrategy() { return this.batchingStrategy; } @@ -336,8 +333,7 @@ public void setBatchingStrategy(BatchingStrategy batchingStrategy) { } @Override - @Nullable - public AcknowledgeMode getAckMode() { + public @Nullable AcknowledgeMode getAckMode() { return this.ackMode; } @@ -346,7 +342,7 @@ public void setAckMode(AcknowledgeMode mode) { } @Override - public ReplyPostProcessor getReplyPostProcessor() { + public @Nullable ReplyPostProcessor getReplyPostProcessor() { return this.replyPostProcessor; } @@ -360,7 +356,7 @@ public void setReplyPostProcessor(ReplyPostProcessor replyPostProcessor) { } @Override - public String getReplyContentType() { + public @Nullable String getReplyContentType() { return this.replyContentType; } @@ -428,7 +424,7 @@ public void setupListenerContainer(MessageListenerContainer listenerContainer) { * @param container the {@link MessageListenerContainer} to create a {@link MessageListener}. * @return a {@link MessageListener} instance. */ - protected abstract MessageListener createMessageListener(MessageListenerContainer container); + protected abstract @Nullable MessageListener createMessageListener(MessageListenerContainer container); private void setupMessageListener(MessageListenerContainer container) { MessageListener messageListener = createMessageListener(container); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java index 9e9fa598a7..88b204cc61 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java @@ -47,6 +47,7 @@ import com.rabbitmq.utility.Utility; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpAuthenticationException; import org.springframework.amqp.AmqpException; @@ -70,7 +71,6 @@ import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.amqp.support.ConsumerTagStrategy; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.lang.Nullable; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -97,14 +97,14 @@ public class BlockingQueueConsumer { private static final int DEFAULT_RETRY_DECLARATION_INTERVAL = 60000; - private static Log logger = LogFactory.getLog(BlockingQueueConsumer.class); + private static final Log logger = LogFactory.getLog(BlockingQueueConsumer.class); private final Lock lifecycleLock = new ReentrantLock(); private final BlockingQueue queue; // When this is non-null the connection has been closed (should never happen in normal operation). - private volatile ShutdownSignalException shutdown; + private volatile @Nullable ShutdownSignalException shutdown; private final String[] queues; @@ -112,8 +112,10 @@ public class BlockingQueueConsumer { private final boolean transactional; + @SuppressWarnings("NullAway.Init") private Channel channel; + @SuppressWarnings("NullAway.Init") private RabbitResourceHolder resourceHolder; private final ConcurrentMap consumers = new ConcurrentHashMap<>(); @@ -155,15 +157,16 @@ public class BlockingQueueConsumer { private long lastRetryDeclaration; - private ConsumerTagStrategy tagStrategy; + private @Nullable ConsumerTagStrategy tagStrategy; + @SuppressWarnings("NullAway.Init") private BackOffExecution backOffExecution; private long shutdownTimeout; private boolean locallyTransacted; - private ApplicationEventPublisher applicationEventPublisher; + private @Nullable ApplicationEventPublisher applicationEventPublisher; private long consumeDelay; @@ -175,11 +178,12 @@ public class BlockingQueueConsumer { private volatile boolean normalCancel; + @SuppressWarnings("NullAway.Init") volatile Thread thread; // NOSONAR package protected volatile boolean declaring; // NOSONAR package protected - private MessageAckListener messageAckListener; + private @Nullable MessageAckListener messageAckListener; /** * Create a consumer. The consumer must not attempt to use @@ -486,8 +490,9 @@ protected boolean cancelled() { * Check if we are in shutdown mode and if so throw an exception. */ private void checkShutdown() { - if (this.shutdown != null) { - throw Utility.fixStackTrace(this.shutdown); + ShutdownSignalException shutdownToUse = this.shutdown; + if (shutdownToUse != null) { + throw Utility.fixStackTrace(shutdownToUse); } } @@ -498,10 +503,10 @@ private void checkShutdown() { * @param delivery the delivered message contents. * @return A message built from the contents. */ - @Nullable - private Message handle(@Nullable Delivery delivery) { - if ((delivery == null && this.shutdown != null)) { - throw this.shutdown; + private @Nullable Message handle(@Nullable Delivery delivery) { + ShutdownSignalException shutdownToUse = this.shutdown; + if (delivery == null && shutdownToUse != null) { + throw shutdownToUse; } if (delivery == null) { return null; @@ -614,6 +619,7 @@ private void checkMissingQueues() { } } + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void start() throws AmqpException { if (logger.isDebugEnabled()) { logger.debug("Starting consumer " + this); @@ -625,7 +631,7 @@ public void start() throws AmqpException { this.resourceHolder = ConnectionFactoryUtils.getTransactionalResourceHolder(this.connectionFactory, this.transactional); this.channel = this.resourceHolder.getChannel(); - ClosingRecoveryListener.addRecoveryListenerIfNecessary(this.channel); // NOSONAR never null here + ClosingRecoveryListener.addRecoveryListenerIfNecessary(this.channel); } catch (AmqpAuthenticationException e) { throw new FatalListenerStartupException("Authentication failure", e); @@ -756,6 +762,7 @@ private void attemptPassiveDeclarations() { } } catch (TimeoutException e1) { + // Ignore } throw new FatalListenerStartupException("Illegal Argument on Queue Declaration", e); } @@ -806,13 +813,11 @@ public void stop() { } public void forceCloseAndClearQueue() { - if (this.channel != null) { - RabbitUtils.setPhysicalCloseRequired(this.channel, true); - ConnectionFactoryUtils.releaseResources(this.resourceHolder); - this.deliveryTags.clear(); - this.consumers.clear(); - this.queue.clear(); // in case we still have a client thread blocked - } + RabbitUtils.setPhysicalCloseRequired(this.channel, true); + ConnectionFactoryUtils.releaseResources(this.resourceHolder); + this.deliveryTags.clear(); + this.consumers.clear(); + this.queue.clear(); // in case we still have a client thread blocked } /** @@ -927,11 +932,13 @@ boolean commitIfNecessary(boolean localTx, boolean forceAck) { * @since 2.4.6 */ private void notifyMessageAckListener(boolean success, long deliveryTag, @Nullable Throwable cause) { - try { - this.messageAckListener.onComplete(success, deliveryTag, cause); - } - catch (Exception e) { - logger.error("An exception occurred in MessageAckListener.", e); + if (this.messageAckListener != null) { + try { + this.messageAckListener.onComplete(success, deliveryTag, cause); + } + catch (Exception e) { + logger.error("An exception occurred in MessageAckListener.", e); + } } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ConditionalRejectingErrorHandler.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ConditionalRejectingErrorHandler.java index 33204d1b64..488a91edc9 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ConditionalRejectingErrorHandler.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ConditionalRejectingErrorHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 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. @@ -22,6 +22,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpRejectAndDontRequeueException; import org.springframework.amqp.ImmediateAcknowledgeAmqpException; @@ -90,7 +91,7 @@ protected boolean isDiscardFatalsWithXDeath() { } /** - * Set to false to disable the (now) default behavior of logging and discarding + * Set to {@code false} to disable the (now) default behavior of logging and discarding * messages that cause fatal exceptions and have an `x-death` header; which * usually means that the message has been republished after previously being * sent to a DLQ. @@ -112,7 +113,7 @@ protected boolean isRejectManual() { } /** - * Set to false to NOT reject a fatal message when MANUAL ack mode is being used. + * Set to {@code false} to NOT reject a fatal message when MANUAL ack mode is being used. * @param rejectManual false to leave the message in an unack'd state. * @since 2.1.9 */ @@ -202,7 +203,7 @@ public static class DefaultExceptionStrategy implements FatalExceptionStrategy { @Override public boolean isFatal(Throwable t) { Throwable cause = t.getCause(); - while ((cause instanceof MessagingException || cause instanceof UndeclaredThrowableException) + while ((cause instanceof MessagingException || cause instanceof UndeclaredThrowableException) && !isCauseFatal(cause)) { cause = cause.getCause(); @@ -214,7 +215,7 @@ public boolean isFatal(Throwable t) { return false; } - private boolean isCauseFatal(Throwable cause) { + private boolean isCauseFatal(@Nullable Throwable cause) { return cause instanceof MessageConversionException // NOSONAR boolean complexity || cause instanceof org.springframework.messaging.converter.MessageConversionException || cause instanceof MethodArgumentResolutionException @@ -230,7 +231,7 @@ private boolean isCauseFatal(Throwable cause) { * @param cause the root cause (skipping any general {@link MessagingException}s). * @since 2.2.4 */ - protected void logFatalException(ListenerExecutionFailedException t, Throwable cause) { + protected void logFatalException(ListenerExecutionFailedException t, @Nullable Throwable cause) { if (this.logger.isWarnEnabled()) { this.logger.warn( "Fatal message conversion error; message rejected; " @@ -244,7 +245,7 @@ protected void logFatalException(ListenerExecutionFailedException t, Throwable c * @param cause the cause * @return true if the cause is fatal. */ - protected boolean isUserCauseFatal(Throwable cause) { + protected boolean isUserCauseFatal(@Nullable Throwable cause) { return false; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java index c7db111bdd..31a210bfba 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectMessageListenerContainer.java @@ -36,9 +36,9 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; -import java.util.stream.Collectors; import java.util.stream.Stream; import com.rabbitmq.client.AMQP.BasicProperties; @@ -47,6 +47,7 @@ import com.rabbitmq.client.Envelope; import com.rabbitmq.client.ShutdownSignalException; import org.apache.commons.logging.Log; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpApplicationContextClosedException; import org.springframework.amqp.AmqpAuthenticationException; @@ -66,12 +67,12 @@ import org.springframework.amqp.rabbit.connection.ConsumerChannelRegistry; import org.springframework.amqp.rabbit.connection.RabbitResourceHolder; import org.springframework.amqp.rabbit.connection.RabbitUtils; +import org.springframework.amqp.rabbit.connection.RoutingConnectionFactory; import org.springframework.amqp.rabbit.connection.SimpleResourceHolder; import org.springframework.amqp.rabbit.listener.support.ContainerUtils; import org.springframework.amqp.rabbit.support.ActiveObjectCounter; import org.springframework.amqp.rabbit.transaction.RabbitTransactionManager; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.lang.Nullable; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.transaction.PlatformTransactionManager; @@ -115,11 +116,11 @@ public class DirectMessageListenerContainer extends AbstractMessageListenerConta private final Set removedQueues = ConcurrentHashMap.newKeySet(); - private final MultiValueMap consumersByQueue = new LinkedMultiValueMap<>(); + private final MultiValueMap consumersByQueue = new LinkedMultiValueMap<>(); private final ActiveObjectCounter cancellationLock = new ActiveObjectCounter<>(); - private TaskScheduler taskScheduler; + private @Nullable TaskScheduler taskScheduler; private boolean taskSchedulerSet; @@ -139,7 +140,7 @@ public class DirectMessageListenerContainer extends AbstractMessageListenerConta private volatile int consumersPerQueue = 1; - private volatile ScheduledFuture consumerMonitorTask; + private volatile @Nullable ScheduledFuture consumerMonitorTask; private volatile long lastAlertAt; @@ -149,6 +150,7 @@ public class DirectMessageListenerContainer extends AbstractMessageListenerConta * Create an instance; {@link #setConnectionFactory(ConnectionFactory)} must * be called before starting. */ + @SuppressWarnings("this-escape") public DirectMessageListenerContainer() { setMissingQueuesFatal(false); doSetPossibleAuthenticationFailureFatal(false); @@ -158,6 +160,7 @@ public DirectMessageListenerContainer() { * Create an instance with the provided connection factory. * @param connectionFactory the connection factory. */ + @SuppressWarnings("this-escape") public DirectMessageListenerContainer(ConnectionFactory connectionFactory) { setConnectionFactory(connectionFactory); setMissingQueuesFatal(false); @@ -202,7 +205,7 @@ public void setTaskScheduler(TaskScheduler taskScheduler) { /** * Set how often to run a task to check for failed consumers and idle containers. - * @param monitorInterval the interval; default 10000 but it will be adjusted down + * @param monitorInterval the interval; default 10000, but it will be adjusted down * to the smallest of this, {@link #setIdleEventInterval(long) idleEventInterval} / 2 * (if configured) or * {@link #setFailedDeclarationRetryInterval(long) failedDeclarationRetryInterval}. @@ -333,6 +336,7 @@ private void removeQueues(Stream queueNames) { } } + @SuppressWarnings("NullAway") // Dataflow analysis limitation private void adjustConsumers(int newCount) { this.consumersLock.lock(); try { @@ -342,11 +346,12 @@ private void adjustConsumers(int newCount) { while (isActive() && (this.consumersByQueue.get(queue) == null || this.consumersByQueue.get(queue).size() < newCount)) { // NOSONAR never null - List cBQ = this.consumersByQueue.get(queue); + List<@Nullable SimpleConsumer> cBQ = this.consumersByQueue.get(queue); int index = 0; if (cBQ != null) { // find a gap or set the index to the end List indices = cBQ.stream() + .filter(Objects::nonNull) .map(SimpleConsumer::getIndex) .sorted() .toList(); @@ -367,7 +372,7 @@ private void adjustConsumers(int newCount) { } private void reduceConsumersIfIdle(int newCount, String queue) { - List consumerList = this.consumersByQueue.get(queue); + List<@Nullable SimpleConsumer> consumerList = this.consumersByQueue.get(queue); if (consumerList != null && consumerList.size() > newCount) { int delta = consumerList.size() - newCount; for (int i = 0; i < delta; i++) { @@ -471,11 +476,11 @@ protected void actualStart() { } } + @SuppressWarnings("try") protected void checkConnect() { if (isPossibleAuthenticationFailureFatal()) { - Connection connection = null; - try { - connection = getConnectionFactory().createConnection(); + try (Connection __ = getConnectionFactory().createConnection()) { + // Authentication attempt } catch (AmqpAuthenticationException ex) { this.logger.debug("Failed to authenticate", ex); @@ -483,15 +488,11 @@ protected void checkConnect() { } catch (Exception ex) { // NOSONAR } - finally { - if (connection != null) { - connection.close(); - } - } } } private void startMonitor(long idleEventInterval, final Map namesToQueues) { + Assert.state(this.taskScheduler != null, "taskScheduler must be provided"); this.consumerMonitorTask = this.taskScheduler.scheduleAtFixedRate(() -> { long now = System.currentTimeMillis(); checkIdle(idleEventInterval, now); @@ -559,7 +560,7 @@ private void checkConsumers(long now) { } return !open; }) - .collect(Collectors.toList()); + .toList(); } finally { this.consumersLock.unlock(); @@ -602,6 +603,7 @@ private boolean restartConsumer(final Map namesToQueues, List list = this.consumersByQueue.get(queue); + List<@Nullable SimpleConsumer> list = this.consumersByQueue.get(queue); // Possible race with setConsumersPerQueue and the task launched by start() if (CollectionUtils.isEmpty(list)) { for (int i = 0; i < this.consumersPerQueue; i++) { @@ -738,8 +744,11 @@ private void doConsumeFromQueue(String queue, int index) { return; } String routingLookupKey = getRoutingLookupKey(); + RoutingConnectionFactory routingConnectionFactory = null; if (routingLookupKey != null) { - SimpleResourceHolder.push(getRoutingConnectionFactory(), routingLookupKey); // NOSONAR both never null here + routingConnectionFactory = getRoutingConnectionFactory(); + Assert.state(routingConnectionFactory != null, "The 'routingConnectionFactory' must be provided"); + SimpleResourceHolder.push(routingConnectionFactory, routingLookupKey); } Connection connection = null; // NOSONAR (close) try { @@ -753,8 +762,8 @@ private void doConsumeFromQueue(String queue, int index) { : new AmqpConnectException(e); } finally { - if (routingLookupKey != null) { - SimpleResourceHolder.pop(getRoutingConnectionFactory()); // NOSONAR never null here + if (routingConnectionFactory != null) { + SimpleResourceHolder.pop(routingConnectionFactory); } } SimpleConsumer consumer = consume(queue, index, connection); @@ -778,8 +787,7 @@ private void doConsumeFromQueue(String queue, int index) { } } - @Nullable - private SimpleConsumer consume(String queue, int index, Connection connection) { + private @Nullable SimpleConsumer consume(String queue, int index, Connection connection) { Channel channel = null; SimpleConsumer consumer = null; try { @@ -815,9 +823,8 @@ private SimpleConsumer consume(String queue, int index, Connection connection) { return consumer; } - @Nullable - private SimpleConsumer handleConsumeException(String queue, int index, @Nullable SimpleConsumer consumerArg, - Exception ex) { + private @Nullable SimpleConsumer handleConsumeException(String queue, int index, + @Nullable SimpleConsumer consumerArg, Exception ex) { SimpleConsumer consumer = consumerArg; if (RabbitUtils.exclusiveAccesssRefused(ex)) { @@ -846,6 +853,7 @@ else if (this.logger.isWarnEnabled()) { } @Override + @SuppressWarnings("NullAway") // Dataflow analysis limitation protected void shutdownAndWaitOrCallback(@Nullable Runnable callback) { LinkedList canceledConsumers = null; boolean waitForConsumers = false; @@ -915,7 +923,6 @@ private void runCallbackIfNotNull(@Nullable Runnable callback) { * @param consumers a copy of this.consumers. */ private void actualShutDown(List consumers) { - Assert.state(getTaskExecutor() != null, "Cannot shut down if not initialized"); this.logger.debug("Shutting down"); if (isForceStop()) { this.stopNow.set(true); @@ -926,8 +933,9 @@ private void actualShutDown(List consumers) { this.consumers.clear(); this.consumersByQueue.clear(); this.logger.debug("All consumers canceled"); - if (this.consumerMonitorTask != null) { - this.consumerMonitorTask.cancel(true); + ScheduledFuture consumerMonitorTaskToUse = this.consumerMonitorTask; + if (consumerMonitorTaskToUse != null) { + consumerMonitorTaskToUse.cancel(true); this.consumerMonitorTask = null; } } @@ -982,7 +990,7 @@ protected final class SimpleConsumer extends DefaultConsumer { private final Log logger = DirectMessageListenerContainer.this.logger; - private final Connection connection; + private final @Nullable Connection connection; private final String queue; @@ -992,7 +1000,7 @@ protected final class SimpleConsumer extends DefaultConsumer { private final ConnectionFactory connectionFactory = getConnectionFactory(); - private final PlatformTransactionManager transactionManager = getTransactionManager(); + private final @Nullable PlatformTransactionManager transactionManager = getTransactionManager(); private final TransactionAttribute transactionAttribute = getTransactionAttribute(); @@ -1002,21 +1010,22 @@ protected final class SimpleConsumer extends DefaultConsumer { private final long ackTimeout = DirectMessageListenerContainer.this.ackTimeout; - private final Channel targetChannel; + private final @Nullable Channel targetChannel; private final Lock lock = new ReentrantLock(); + private final AtomicInteger epoch = new AtomicInteger(0); + private int pendingAcks; private long lastAck = System.currentTimeMillis(); private long latestDeferredDeliveryTag; + @SuppressWarnings("NullAway.Init") private volatile String consumerTag; - private volatile int epoch; - - private volatile TransactionTemplate transactionTemplate; + private volatile @Nullable TransactionTemplate transactionTemplate; private volatile boolean canceled; @@ -1054,7 +1063,7 @@ public String getConsumerTag() { * @return the epoch. */ int getEpoch() { - return this.epoch; + return this.epoch.get(); } /** @@ -1089,7 +1098,7 @@ boolean targetChanged() { * @return the epoch. */ int incrementAndGetEpoch() { - return ++this.epoch; + return this.epoch.incrementAndGet(); } @Override @@ -1146,6 +1155,7 @@ public void handleDelivery(String consumerTag, Envelope envelope, } } + @SuppressWarnings("NullAway") // Dataflow analysis limitation private void executeListenerInTransaction(Object data, long deliveryTag) { if (this.isRabbitTxManager) { ConsumerChannelRegistry.registerConsumerChannel(getChannel(), this.connectionFactory); @@ -1377,7 +1387,8 @@ void cancelConsumer(final String eventMessage) { publishConsumerFailedEvent(eventMessage, true, null); DirectMessageListenerContainer.this.consumersLock.lock(); try { - List list = DirectMessageListenerContainer.this.consumersByQueue.get(this.queue); + List<@Nullable SimpleConsumer> list = + DirectMessageListenerContainer.this.consumersByQueue.get(this.queue); if (list != null) { list.remove(this); } @@ -1408,7 +1419,7 @@ public int hashCode() { int result = 1; result = prime * result + getEnclosingInstance().hashCode(); result = prime * result + this.index; - result = prime * result + ((this.queue == null) ? 0 : this.queue.hashCode()); + result = prime * result + this.queue.hashCode(); return result; } @@ -1430,12 +1441,7 @@ public boolean equals(Object obj) { if (this.index != other.index) { return false; } - if (this.queue == null) { - return other.queue == null; - } - else { - return this.queue.equals(other.queue); - } + return this.queue.equals(other.queue); } private DirectMessageListenerContainer getEnclosingInstance() { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainer.java index 15a95465c4..2826493aee 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/DirectReplyToMessageListenerContainer.java @@ -21,6 +21,7 @@ import java.util.concurrent.atomic.AtomicInteger; import com.rabbitmq.client.Channel; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpTimeoutException; import org.springframework.amqp.core.AcknowledgeMode; @@ -28,7 +29,6 @@ import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -51,6 +51,7 @@ public class DirectReplyToMessageListenerContainer extends DirectMessageListener private final AtomicInteger consumerCount = new AtomicInteger(); + @SuppressWarnings("this-escape") public DirectReplyToMessageListenerContainer(ConnectionFactory connectionFactory) { super(connectionFactory); super.setQueueNames(Address.AMQ_RABBITMQ_REPLY_TO); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerContainerConsumerFailedEvent.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerContainerConsumerFailedEvent.java index 3508d99c5a..a136e6e5cb 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerContainerConsumerFailedEvent.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerContainerConsumerFailedEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-2025 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. @@ -16,25 +16,31 @@ package org.springframework.amqp.rabbit.listener; +import java.io.Serial; + +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.event.AmqpEvent; -import org.springframework.lang.Nullable; /** * Published when a listener consumer fails. * * @author Gary Russell + * @author Artem Bilan + * * @since 1.5 * */ public class ListenerContainerConsumerFailedEvent extends AmqpEvent { + @Serial private static final long serialVersionUID = -8122166328567190605L; - private final String reason; + private final @Nullable String reason; private final boolean fatal; - private final Throwable throwable; + private final @Nullable Throwable throwable; /** * Construct an instance with the provided arguments. @@ -43,7 +49,7 @@ public class ListenerContainerConsumerFailedEvent extends AmqpEvent { * @param throwable the throwable. * @param fatal true if the startup failure was fatal (will not be retried). */ - public ListenerContainerConsumerFailedEvent(Object source, String reason, + public ListenerContainerConsumerFailedEvent(Object source, @Nullable String reason, @Nullable Throwable throwable, boolean fatal) { super(source); this.reason = reason; @@ -51,7 +57,7 @@ public ListenerContainerConsumerFailedEvent(Object source, String reason, this.throwable = throwable; } - public String getReason() { + public @Nullable String getReason() { return this.reason; } @@ -59,7 +65,7 @@ public boolean isFatal() { return this.fatal; } - public Throwable getThrowable() { + public @Nullable Throwable getThrowable() { return this.throwable; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerContainerConsumerTerminatedEvent.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerContainerConsumerTerminatedEvent.java index 1a392522f6..012c12262d 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerContainerConsumerTerminatedEvent.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerContainerConsumerTerminatedEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-2025 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. @@ -16,6 +16,10 @@ package org.springframework.amqp.rabbit.listener; +import java.io.Serial; + +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.event.AmqpEvent; /** @@ -27,21 +31,22 @@ */ public class ListenerContainerConsumerTerminatedEvent extends AmqpEvent { + @Serial private static final long serialVersionUID = -8122166328567190605L; - private final String reason; + private final @Nullable String reason; /** * Construct an instance with the provided arguments. * @param source the source container. * @param reason the reason. */ - public ListenerContainerConsumerTerminatedEvent(Object source, String reason) { + public ListenerContainerConsumerTerminatedEvent(Object source, @Nullable String reason) { super(source); this.reason = reason; } - public String getReason() { + public @Nullable String getReason() { return this.reason; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerContainerIdleEvent.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerContainerIdleEvent.java index ec30cf19a7..d978ac15dd 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerContainerIdleEvent.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerContainerIdleEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 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. @@ -20,14 +20,17 @@ import java.util.Arrays; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.event.AmqpEvent; -import org.springframework.lang.Nullable; /** * An event that is emitted when a container is idle if the container * is configured to do so. * * @author Gary Russell + * @author Artem Bilan + * * @since 1.6 * */ @@ -36,8 +39,7 @@ public class ListenerContainerIdleEvent extends AmqpEvent { private final long idleTime; - @Nullable - private final String listenerId; + private final @Nullable String listenerId; private final List queueNames; @@ -61,7 +63,7 @@ public long getIdleTime() { * @return the queue names. */ public String[] getQueueNames() { - return this.queueNames.toArray(new String[this.queueNames.size()]); + return this.queueNames.toArray(new String[0]); } /** diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerFailedRuleBasedTransactionAttribute.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerFailedRuleBasedTransactionAttribute.java index 30061b0e74..a84e2d1c05 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerFailedRuleBasedTransactionAttribute.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ListenerFailedRuleBasedTransactionAttribute.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 the original author or authors. + * Copyright 2016-2025 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. @@ -25,6 +25,8 @@ * Allows users to control rollback based on the actual cause. * * @author Gary Russell + * @author Artem Bilan + * * @since 1.6.6 * */ @@ -33,7 +35,7 @@ public class ListenerFailedRuleBasedTransactionAttribute extends RuleBasedTransa @Override public boolean rollbackOn(Throwable ex) { - if (ex instanceof ListenerExecutionFailedException) { + if (ex instanceof ListenerExecutionFailedException && ex.getCause() != null) { return super.rollbackOn(ex.getCause()); } else { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MessageAckListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MessageAckListener.java index 3c63904e19..90d16e8d2d 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MessageAckListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MessageAckListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2025 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. @@ -16,14 +16,15 @@ package org.springframework.amqp.rabbit.listener; -import org.springframework.amqp.core.AcknowledgeMode; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** - * A listener for message ack when using {@link AcknowledgeMode#AUTO}. + * A listener for message ack when using {@link org.springframework.amqp.core.AcknowledgeMode#AUTO}. * * @author Cao Weibo * @author Gary Russell + * @author Artem Bilan + * * @since 2.4.6 */ @FunctionalInterface diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MessageListenerContainer.java index 52d24b2b6a..acbe69d6fe 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2021 the original author or authors. + * Copyright 2014-2025 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. @@ -16,10 +16,11 @@ package org.springframework.amqp.rabbit.listener; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.MessageListener; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.SmartLifecycle; -import org.springframework.lang.Nullable; /** * Internal abstraction used by the framework representing a message @@ -32,7 +33,7 @@ public interface MessageListenerContainer extends SmartLifecycle, InitializingBean { /** - * Setup the message listener to use. Throws an {@link IllegalArgumentException} + * Set up the message listener to use. Throws an {@link IllegalArgumentException} * if that message listener type is not supported. * @param messageListener the {@code object} to wrapped to the {@code MessageListener}. */ diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MethodRabbitListenerEndpoint.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MethodRabbitListenerEndpoint.java index 171b1005cd..e9570f4cb0 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MethodRabbitListenerEndpoint.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MethodRabbitListenerEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 the original author or authors. + * Copyright 2014-2025 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. @@ -19,14 +19,17 @@ import java.lang.reflect.Method; import java.util.Arrays; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.rabbit.batch.BatchingStrategy; import org.springframework.amqp.rabbit.listener.adapter.BatchMessagingMessageListenerAdapter; import org.springframework.amqp.rabbit.listener.adapter.HandlerAdapter; import org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter; import org.springframework.amqp.rabbit.listener.api.RabbitListenerErrorHandler; import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.beans.factory.config.BeanExpressionContext; +import org.springframework.beans.factory.config.BeanExpressionResolver; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.lang.Nullable; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory; import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; @@ -44,15 +47,17 @@ */ public class MethodRabbitListenerEndpoint extends AbstractRabbitListenerEndpoint { + @SuppressWarnings("NullAway.Init") private Object bean; - private Method method; + private @Nullable Method method; + @SuppressWarnings("NullAway.Init") private MessageHandlerMethodFactory messageHandlerMethodFactory; private boolean returnExceptions; - private RabbitListenerErrorHandler errorHandler; + private @Nullable RabbitListenerErrorHandler errorHandler; private AdapterProvider adapterProvider = new DefaultAdapterProvider(); @@ -76,7 +81,7 @@ public void setMethod(Method method) { this.method = method; } - public Method getMethod() { + public @Nullable Method getMethod() { return this.method; } @@ -113,7 +118,7 @@ public void setErrorHandler(RabbitListenerErrorHandler errorHandler) { /** * @return the messageHandlerMethodFactory */ - protected MessageHandlerMethodFactory getMessageHandlerMethodFactory() { + protected @Nullable MessageHandlerMethodFactory getMessageHandlerMethodFactory() { return this.messageHandlerMethodFactory; } @@ -128,8 +133,6 @@ public void setAdapterProvider(AdapterProvider adapterProvider) { @Override protected MessagingMessageListenerAdapter createMessageListener(MessageListenerContainer container) { - Assert.state(this.messageHandlerMethodFactory != null, - "Could not create message listener - MessageHandlerMethodFactory not set"); MessagingMessageListenerAdapter messageListener = createMessageListenerInstance(getBatchListener()); messageListener.setHandlerAdapter(configureListenerAdapter(messageListener)); String replyToAddress = getDefaultReplyToAddress(); @@ -140,9 +143,7 @@ protected MessagingMessageListenerAdapter createMessageListener(MessageListenerC if (messageConverter != null) { messageListener.setMessageConverter(messageConverter); } - if (getBeanResolver() != null) { - messageListener.setBeanResolver(getBeanResolver()); - } + messageListener.setBeanResolver(getBeanResolver()); return messageListener; } @@ -152,8 +153,10 @@ protected MessagingMessageListenerAdapter createMessageListener(MessageListenerC * @return the handler adapter. */ protected HandlerAdapter configureListenerAdapter(MessagingMessageListenerAdapter messageListener) { + Method methodToUse = getMethod(); + Assert.notNull(methodToUse, "'method' must be provided"); InvocableHandlerMethod invocableHandlerMethod = - this.messageHandlerMethodFactory.createInvocableHandlerMethod(getBean(), getMethod()); + this.messageHandlerMethodFactory.createInvocableHandlerMethod(getBean(), methodToUse); return new HandlerAdapter(invocableHandlerMethod); } @@ -167,8 +170,7 @@ protected MessagingMessageListenerAdapter createMessageListenerInstance(@Nullabl this.returnExceptions, this.errorHandler, getBatchingStrategy()); } - @Nullable - private String getDefaultReplyToAddress() { + private @Nullable String getDefaultReplyToAddress() { Method listenerMethod = getMethod(); if (listenerMethod != null) { SendTo ann = AnnotationUtils.getAnnotation(listenerMethod, SendTo.class); @@ -184,16 +186,19 @@ private String getDefaultReplyToAddress() { return null; } - private String resolveSendTo(String value) { - if (getBeanFactory() != null) { - String resolvedValue = getBeanExpressionContext().getBeanFactory().resolveEmbeddedValue(value); - Object newValue = getResolver().evaluate(resolvedValue, getBeanExpressionContext()); - Assert.isInstanceOf(String.class, newValue, "Invalid @SendTo expression"); - return (String) newValue; - } - else { - return value; + private @Nullable String resolveSendTo(String value) { + BeanExpressionContext beanExpressionContext = getBeanExpressionContext(); + if (beanExpressionContext != null) { + String resolvedValue = beanExpressionContext.getBeanFactory().resolveEmbeddedValue(value); + BeanExpressionResolver resolverToUse = getResolver(); + if (resolverToUse != null) { + Object newValue = resolverToUse.evaluate(resolvedValue, beanExpressionContext); + Assert.isInstanceOf(String.class, newValue, "Invalid @SendTo expression"); + return (String) newValue; + } } + + return value; } @Override @@ -220,15 +225,17 @@ public interface AdapterProvider { * @param batchingStrategy the batching strategy for batch listeners. * @return the adapter. */ - MessagingMessageListenerAdapter getAdapter(boolean batch, Object bean, Method method, boolean returnExceptions, - RabbitListenerErrorHandler errorHandler, @Nullable BatchingStrategy batchingStrategy); + MessagingMessageListenerAdapter getAdapter(boolean batch, @Nullable Object bean, @Nullable Method method, + boolean returnExceptions, @Nullable RabbitListenerErrorHandler errorHandler, + @Nullable BatchingStrategy batchingStrategy); + } private static final class DefaultAdapterProvider implements AdapterProvider { @Override - public MessagingMessageListenerAdapter getAdapter(boolean batch, Object bean, Method method, - boolean returnExceptions, RabbitListenerErrorHandler errorHandler, + public MessagingMessageListenerAdapter getAdapter(boolean batch, @Nullable Object bean, @Nullable Method method, + boolean returnExceptions, @Nullable RabbitListenerErrorHandler errorHandler, @Nullable BatchingStrategy batchingStrategy) { if (batch) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MicrometerHolder.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MicrometerHolder.java index 422ffc67bf..eb70aa8968 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MicrometerHolder.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MicrometerHolder.java @@ -24,16 +24,20 @@ import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.Timer.Builder; import io.micrometer.core.instrument.Timer.Sample; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.context.ApplicationContext; -import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; /** * Abstraction to avoid hard reference to Micrometer. * * @author Gary Russell * @author Ngoc Nhan + * @author Artem Bilan + * * @since 2.4.6 * */ @@ -41,16 +45,16 @@ public final class MicrometerHolder { private final ConcurrentMap timers = new ConcurrentHashMap<>(); + @SuppressWarnings("NullAway.Init") private final MeterRegistry registry; private final Map tags; private final String listenerId; + @SuppressWarnings("NullAway") // Dataflow analysis limitation MicrometerHolder(@Nullable ApplicationContext context, String listenerId, Map tags) { - if (context == null) { - throw new IllegalStateException("No micrometer registry present"); - } + Assert.notNull(context, "'context' must not be null"); try { this.registry = context.getBeanProvider(MeterRegistry.class).getIfUnique(); } @@ -95,7 +99,7 @@ private Timer buildTimer(String aListenerId, String result, String queue, String .tag("queue", queue) .tag("result", result) .tag("exception", exception); - if (this.tags != null && !this.tags.isEmpty()) { + if (!CollectionUtils.isEmpty(this.tags)) { this.tags.forEach(builder::tag); } Timer registeredTimer = builder.register(this.registry); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MissingQueueEvent.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MissingQueueEvent.java index e02437874e..df09860c1a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MissingQueueEvent.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MissingQueueEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 the original author or authors. + * Copyright 2021-2025 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. @@ -16,6 +16,8 @@ package org.springframework.amqp.rabbit.listener; +import java.io.Serial; + import org.springframework.amqp.event.AmqpEvent; /** @@ -27,6 +29,7 @@ */ public class MissingQueueEvent extends AmqpEvent { + @Serial private static final long serialVersionUID = 1L; private final String queue; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MultiMethodRabbitListenerEndpoint.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MultiMethodRabbitListenerEndpoint.java index 1833eee771..c1c578fef1 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MultiMethodRabbitListenerEndpoint.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/MultiMethodRabbitListenerEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2024 the original author or authors. + * Copyright 2015-2025 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. @@ -20,16 +20,21 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.rabbit.listener.adapter.DelegatingInvocableHandler; import org.springframework.amqp.rabbit.listener.adapter.HandlerAdapter; import org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter; -import org.springframework.lang.Nullable; +import org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory; import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; +import org.springframework.util.Assert; import org.springframework.validation.Validator; /** * @author Gary Russell * @author Ngoc Nhan + * @author Artem Bilan + * * @since 1.5 * */ @@ -37,9 +42,9 @@ public class MultiMethodRabbitListenerEndpoint extends MethodRabbitListenerEndpo private final List methods; - private final Method defaultMethod; + private final @Nullable Method defaultMethod; - private Validator validator; + private @Nullable Validator validator; /** * Construct an instance for the provided methods, default method and bean. @@ -48,6 +53,7 @@ public class MultiMethodRabbitListenerEndpoint extends MethodRabbitListenerEndpo * @param bean the bean. * @since 2.0.3 */ + @SuppressWarnings("this-escape") public MultiMethodRabbitListenerEndpoint(List methods, @Nullable Method defaultMethod, Object bean) { this.methods = methods; this.defaultMethod = defaultMethod; @@ -67,9 +73,13 @@ public void setValidator(Validator validator) { protected HandlerAdapter configureListenerAdapter(MessagingMessageListenerAdapter messageListener) { List invocableHandlerMethods = new ArrayList<>(); InvocableHandlerMethod defaultHandler = null; + MessageHandlerMethodFactory messageHandlerMethodFactory = getMessageHandlerMethodFactory(); + Assert.state(messageHandlerMethodFactory != null, + "Could not create message listener - MessageHandlerMethodFactory not set"); + Object beanToUse = getBean(); for (Method method : this.methods) { - InvocableHandlerMethod handler = getMessageHandlerMethodFactory() - .createInvocableHandlerMethod(getBean(), method); + InvocableHandlerMethod handler = messageHandlerMethodFactory + .createInvocableHandlerMethod(beanToUse, method); invocableHandlerMethods.add(handler); if (method.equals(this.defaultMethod)) { defaultHandler = handler; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ObservableListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ObservableListenerContainer.java index e9a7b7ba32..21c04d5ab0 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ObservableListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ObservableListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2023-2025 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. @@ -19,16 +19,19 @@ import java.util.HashMap; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.rabbit.connection.RabbitAccessor; import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.DisposableBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** * @author Gary Russell + * @author Artem Bilan + * * @since 3.0.5 * */ @@ -36,13 +39,13 @@ public abstract class ObservableListenerContainer extends RabbitAccessor implements MessageListenerContainer, ApplicationContextAware, BeanNameAware, DisposableBean { private static final boolean MICROMETER_PRESENT = ClassUtils.isPresent( - "io.micrometer.core.instrument.MeterRegistry", AbstractMessageListenerContainer.class.getClassLoader()); + "io.micrometer.core.instrument.MeterRegistry", AbstractMessageListenerContainer.class.getClassLoader()); - private ApplicationContext applicationContext; + private @Nullable ApplicationContext applicationContext; private final Map micrometerTags = new HashMap<>(); - private MicrometerHolder micrometerHolder; + private @Nullable MicrometerHolder micrometerHolder; private boolean micrometerEnabled = true; @@ -50,10 +53,9 @@ public abstract class ObservableListenerContainer extends RabbitAccessor private String beanName = "not.a.Spring.bean"; - private String listenerId; + private @Nullable String listenerId; - @Nullable - protected final ApplicationContext getApplicationContext() { + protected final @Nullable ApplicationContext getApplicationContext() { return this.applicationContext; } @@ -62,7 +64,7 @@ public final void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } - protected MicrometerHolder getMicrometerHolder() { + protected @Nullable MicrometerHolder getMicrometerHolder() { return this.micrometerHolder; } @@ -71,14 +73,14 @@ protected MicrometerHolder getMicrometerHolder() { * @param tags the tags. * @since 2.2 */ - public void setMicrometerTags(Map tags) { + public void setMicrometerTags(@Nullable Map tags) { if (tags != null) { this.micrometerTags.putAll(tags); } } /** - * Set to false to disable micrometer listener timers. When true, ignored + * Set to {@code false} to disable micrometer listener timers. When true, ignored * if {@link #setObservationEnabled(boolean)} is set to true. * @param micrometerEnabled false to disable. * @since 2.2 @@ -127,7 +129,6 @@ public void setBeanName(String beanName) { /** * @return The bean name that this listener container has been assigned in its containing bean factory, if any. */ - @Nullable protected final String getBeanName() { return this.beanName; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerContainerFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerContainerFactory.java index 4b1b1ae320..ce8068f25f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerContainerFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerContainerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -16,8 +16,9 @@ package org.springframework.amqp.rabbit.listener; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanNameAware; -import org.springframework.lang.Nullable; /** * Factory of {@link MessageListenerContainer}s. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpoint.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpoint.java index 062f40a524..284aace533 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpoint.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 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. @@ -16,12 +16,13 @@ package org.springframework.amqp.rabbit.listener; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.rabbit.batch.BatchingStrategy; import org.springframework.amqp.rabbit.listener.adapter.ReplyPostProcessor; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.core.task.TaskExecutor; -import org.springframework.lang.Nullable; /** * Model for a Rabbit listener endpoint. Can be used against a @@ -40,18 +41,21 @@ public interface RabbitListenerEndpoint { * container. * @see RabbitListenerContainerFactory#createListenerContainer */ + @Nullable String getId(); /** * @return the group of this endpoint or null if not in a group. * @since 1.5 */ + @Nullable String getGroup(); /** * @return the concurrency of this endpoint. * @since 2.0 */ + @Nullable String getConcurrency(); /** @@ -59,10 +63,11 @@ public interface RabbitListenerEndpoint { * @return the autoStartup. * @since 2.0 */ + @Nullable Boolean getAutoStartup(); /** - * Setup the specified message listener container with the model + * Set up the specified message listener container with the model * defined by this endpoint. *

This endpoint must provide the requested missing option(s) of * the specified container to make it usable. Usually, this is about @@ -92,8 +97,7 @@ default void setMessageConverter(MessageConverter converter) { * @return the converter. * @since 2.0.8 */ - @Nullable - default MessageConverter getMessageConverter() { + default @Nullable MessageConverter getMessageConverter() { return null; } @@ -103,8 +107,7 @@ default MessageConverter getMessageConverter() { * @return the executor. * @since 2.2 */ - @Nullable - default TaskExecutor getTaskExecutor() { + default @Nullable TaskExecutor getTaskExecutor() { return null; } @@ -138,8 +141,7 @@ default void setBatchingStrategy(BatchingStrategy batchingStrategy) { * @return the strategy. * @since 2.4.7 */ - @Nullable - default BatchingStrategy getBatchingStrategy() { + default @Nullable BatchingStrategy getBatchingStrategy() { return null; } @@ -148,8 +150,7 @@ default BatchingStrategy getBatchingStrategy() { * @return the acknowledgment mode. * @since 2.2 */ - @Nullable - default AcknowledgeMode getAckMode() { + default @Nullable AcknowledgeMode getAckMode() { return null; } @@ -159,8 +160,7 @@ default AcknowledgeMode getAckMode() { * @return the post processor. * @since 2.2.5 */ - @Nullable - default ReplyPostProcessor getReplyPostProcessor() { + default @Nullable ReplyPostProcessor getReplyPostProcessor() { return null; } @@ -169,8 +169,7 @@ default ReplyPostProcessor getReplyPostProcessor() { * @return the content type. * @since 2.3 */ - @Nullable - default String getReplyContentType() { + default @Nullable String getReplyContentType() { return null; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistrar.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistrar.java index 6075b7603a..d0d8492259 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistrar.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 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. @@ -23,10 +23,11 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory; import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; import org.springframework.util.Assert; @@ -52,20 +53,20 @@ public class RabbitListenerEndpointRegistrar implements BeanFactoryAware, Initia private List customMethodArgumentResolvers = new ArrayList<>(); - @Nullable - private RabbitListenerEndpointRegistry endpointRegistry; + private @Nullable RabbitListenerEndpointRegistry endpointRegistry; - private MessageHandlerMethodFactory messageHandlerMethodFactory; + private @Nullable MessageHandlerMethodFactory messageHandlerMethodFactory; - private RabbitListenerContainerFactory containerFactory; + private @Nullable RabbitListenerContainerFactory containerFactory; - private String containerFactoryBeanName; + private @Nullable String containerFactoryBeanName; + @SuppressWarnings("NullAway.Init") private BeanFactory beanFactory; private boolean startImmediately; - private Validator validator; + private @Nullable Validator validator; /** * Set the {@link RabbitListenerEndpointRegistry} instance to use. @@ -79,8 +80,7 @@ public void setEndpointRegistry(RabbitListenerEndpointRegistry endpointRegistry) * @return the {@link RabbitListenerEndpointRegistry} instance for this * registrar, may be {@code null}. */ - @Nullable - public RabbitListenerEndpointRegistry getEndpointRegistry() { + public @Nullable RabbitListenerEndpointRegistry getEndpointRegistry() { return this.endpointRegistry; } @@ -93,7 +93,6 @@ public List getCustomMethodArgumentResolvers() { return Collections.unmodifiableList(this.customMethodArgumentResolvers); } - /** * Add custom methods arguments resolvers to * {@link org.springframework.amqp.rabbit.annotation.RabbitListenerAnnotationBeanPostProcessor} @@ -111,7 +110,7 @@ public void setCustomMethodArgumentResolvers(HandlerMethodArgumentResolver... me *

* By default, * {@link org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory} - * is used and it can be configured further to support additional method arguments or + * is used, and it can be configured further to support additional method arguments or * to customize conversion and validation support. See * {@link org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory} * javadoc for more details. @@ -125,7 +124,7 @@ public void setMessageHandlerMethodFactory(MessageHandlerMethodFactory rabbitHan /** * @return the custom {@link MessageHandlerMethodFactory} to use, if any. */ - public MessageHandlerMethodFactory getMessageHandlerMethodFactory() { + public @Nullable MessageHandlerMethodFactory getMessageHandlerMethodFactory() { return this.messageHandlerMethodFactory; } @@ -167,8 +166,7 @@ public void setBeanFactory(BeanFactory beanFactory) { * @return the validator. * @since 2.3.7 */ - @Nullable - public Validator getValidator() { + public @Nullable Validator getValidator() { return this.validator; } @@ -214,9 +212,8 @@ else if (this.containerFactory != null) { return this.containerFactory; } else if (this.containerFactoryBeanName != null) { - Assert.state(this.beanFactory != null, "BeanFactory must be set to obtain container factory by bean name"); - this.containerFactory = this.beanFactory.getBean( - this.containerFactoryBeanName, RabbitListenerContainerFactory.class); + this.containerFactory = + this.beanFactory.getBean(this.containerFactoryBeanName, RabbitListenerContainerFactory.class); return this.containerFactory; // Consider changing this if live change of the factory is required } else { @@ -234,8 +231,10 @@ else if (this.containerFactoryBeanName != null) { * @param endpoint the {@link RabbitListenerEndpoint} instance to register. * @param factory the {@link RabbitListenerContainerFactory} to use. */ + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void registerEndpoint(RabbitListenerEndpoint endpoint, @Nullable RabbitListenerContainerFactory factory) { + Assert.notNull(endpoint, "Endpoint must be set"); Assert.hasText(endpoint.getId(), "Endpoint id must be set"); Assert.state(!this.startImmediately || this.endpointRegistry != null, "No registry available"); @@ -267,16 +266,9 @@ public void registerEndpoint(RabbitListenerEndpoint endpoint) { registerEndpoint(endpoint, null); } - private record AmqpListenerEndpointDescriptor(RabbitListenerEndpoint endpoint, - RabbitListenerContainerFactory containerFactory) { + @Nullable RabbitListenerContainerFactory containerFactory) { - private AmqpListenerEndpointDescriptor(RabbitListenerEndpoint endpoint, - @Nullable RabbitListenerContainerFactory containerFactory) { - this.endpoint = endpoint; - this.containerFactory = containerFactory; - } - - } + } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistry.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistry.java index cf3b2c2615..e0dcdede30 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistry.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpointRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2025 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. @@ -29,6 +29,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.factory.DisposableBean; @@ -38,7 +39,6 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.SmartLifecycle; import org.springframework.context.event.ContextRefreshedEvent; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -77,11 +77,10 @@ public class RabbitListenerEndpointRegistry implements DisposableBean, SmartLife private int phase = Integer.MAX_VALUE; - private ConfigurableApplicationContext applicationContext; + private @Nullable ConfigurableApplicationContext applicationContext; private boolean contextRefreshed; - @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { if (applicationContext instanceof ConfigurableApplicationContext configurable) { @@ -97,7 +96,7 @@ public void setApplicationContext(ApplicationContext applicationContext) throws * @see RabbitListenerEndpoint#getId() * @see #getListenerContainerIds() */ - public MessageListenerContainer getListenerContainer(String id) { + public @Nullable MessageListenerContainer getListenerContainer(String id) { Assert.hasText(id, "Container identifier must not be empty"); return this.listenerContainers.get(id); } @@ -141,9 +140,9 @@ public void registerListenerContainer(RabbitListenerEndpoint endpoint, RabbitLis * @see #getListenerContainers() * @see #getListenerContainer(String) */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "NullAway"}) // Dataflow analysis limitation public void registerListenerContainer(RabbitListenerEndpoint endpoint, RabbitListenerContainerFactory factory, - boolean startImmediately) { + boolean startImmediately) { Assert.notNull(endpoint, "Endpoint must not be null"); Assert.notNull(factory, "Factory must not be null"); @@ -228,7 +227,6 @@ public void destroy() { } } - // Delegating implementation of SmartLifecycle @Override @@ -298,7 +296,6 @@ private void startIfNecessary(MessageListenerContainer listenerContainer) { } } - @Override public void onApplicationEvent(ContextRefreshedEvent event) { if (event.getApplicationContext().equals(this.applicationContext)) { @@ -306,7 +303,6 @@ public void onApplicationEvent(ContextRefreshedEvent event) { } } - private static final class AggregatingCallback implements Runnable { private final AtomicInteger count; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java index 06471602b3..4902c16e3f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java @@ -33,6 +33,7 @@ import com.rabbitmq.client.Channel; import com.rabbitmq.client.PossibleAuthenticationFailureException; import com.rabbitmq.client.ShutdownSignalException; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpAuthenticationException; import org.springframework.amqp.AmqpConnectException; @@ -44,6 +45,7 @@ import org.springframework.amqp.core.BatchMessageListener; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessagePostProcessor; +import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.core.Queue; import org.springframework.amqp.rabbit.batch.BatchingStrategy; import org.springframework.amqp.rabbit.connection.ConnectionFactory; @@ -51,6 +53,7 @@ import org.springframework.amqp.rabbit.connection.ConsumerChannelRegistry; import org.springframework.amqp.rabbit.connection.RabbitResourceHolder; import org.springframework.amqp.rabbit.connection.RabbitUtils; +import org.springframework.amqp.rabbit.connection.RoutingConnectionFactory; import org.springframework.amqp.rabbit.connection.SimpleResourceHolder; import org.springframework.amqp.rabbit.listener.api.ChannelAwareBatchMessageListener; import org.springframework.amqp.rabbit.listener.exception.FatalListenerExecutionException; @@ -64,7 +67,6 @@ import org.springframework.core.log.LogMessage; import org.springframework.jmx.export.annotation.ManagedMetric; import org.springframework.jmx.support.MetricType; -import org.springframework.lang.Nullable; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.transaction.support.TransactionTemplate; @@ -105,7 +107,7 @@ public class SimpleMessageListenerContainer extends AbstractMessageListenerConta private final AtomicLong lastNoMessageAlert = new AtomicLong(); - private final AtomicReference containerStoppingForAbort = new AtomicReference<>(); + private final AtomicReference<@Nullable Thread> containerStoppingForAbort = new AtomicReference<>(); private final BlockingQueue abortEvents = new LinkedBlockingQueue<>(); @@ -127,13 +129,13 @@ public class SimpleMessageListenerContainer extends AbstractMessageListenerConta private long batchReceiveTimeout; - private Set consumers; + private @Nullable Set consumers; - private Integer declarationRetries; + private @Nullable Integer declarationRetries; - private Long retryDeclarationInterval; + private @Nullable Long retryDeclarationInterval; - private TransactionTemplate transactionTemplate; + private @Nullable TransactionTemplate transactionTemplate; private long consumerStartTimeout = DEFAULT_CONSUMER_START_TIMEOUT; @@ -141,7 +143,7 @@ public class SimpleMessageListenerContainer extends AbstractMessageListenerConta private volatile int concurrentConsumers = 1; - private volatile Integer maxConcurrentConsumers; + private volatile @Nullable Integer maxConcurrentConsumers; private volatile long lastConsumerStarted; @@ -158,6 +160,7 @@ public SimpleMessageListenerContainer() { * * @param connectionFactory the {@link ConnectionFactory} */ + @SuppressWarnings("this-escape") public SimpleMessageListenerContainer(ConnectionFactory connectionFactory) { setConnectionFactory(connectionFactory); } @@ -175,8 +178,9 @@ public void setConcurrentConsumers(final int concurrentConsumers) { Assert.isTrue(concurrentConsumers > 0, "'concurrentConsumers' value must be at least 1 (one)"); Assert.isTrue(!isExclusive() || concurrentConsumers == 1, "When the consumer is exclusive, the concurrency must be 1"); - if (this.maxConcurrentConsumers != null) { - Assert.isTrue(concurrentConsumers <= this.maxConcurrentConsumers, + Integer maxConcurrentConsumersToCheck = this.maxConcurrentConsumers; + if (maxConcurrentConsumersToCheck != null) { + Assert.isTrue(concurrentConsumers <= maxConcurrentConsumersToCheck, "'concurrentConsumers' cannot be more than 'maxConcurrentConsumers'"); } this.consumersLock.lock(); @@ -258,8 +262,9 @@ public void setConcurrency(String concurrency) { */ @Override public final void setExclusive(boolean exclusive) { + Integer maxConcurrentConsumersToCheck = this.maxConcurrentConsumers; Assert.isTrue(!exclusive || (this.concurrentConsumers == 1 - && (this.maxConcurrentConsumers == null || this.maxConcurrentConsumers == 1)), + && (maxConcurrentConsumersToCheck == null || maxConcurrentConsumersToCheck == 1)), "When the consumer is exclusive, the concurrency must be 1"); super.setExclusive(exclusive); } @@ -630,7 +635,7 @@ private void checkListenerContainerAware() { private void waitForConsumersToStart(Set processors) { for (AsyncMessageProcessingConsumer processor : processors) { - FatalListenerStartupException startupException = null; + FatalListenerStartupException startupException; try { startupException = processor.getStartupException(); } @@ -779,9 +784,13 @@ protected void adjustConsumers(int deltaArg) { if (isActive() && this.consumers != null) { if (delta > 0) { Iterator consumerIterator = this.consumers.iterator(); - while (consumerIterator.hasNext() && delta > 0 - && (this.maxConcurrentConsumers == null - || this.consumers.size() > this.maxConcurrentConsumers)) { + Integer maxConcurrentConsumersToCheck = this.maxConcurrentConsumers; + while (consumerIterator.hasNext() && delta > 0) { + if (!(maxConcurrentConsumersToCheck == null + || this.consumers.size() > maxConcurrentConsumersToCheck)) { + + break; + } BlockingQueueConsumer consumer = consumerIterator.next(); consumer.basicCancel(true); consumerIterator.remove(); @@ -807,9 +816,10 @@ protected void addAndStartConsumers(int delta) { this.consumersLock.lock(); try { if (this.consumers != null) { + Integer maxConcurrentConsumersToCheck = this.maxConcurrentConsumers; for (int i = 0; i < delta; i++) { - if (this.maxConcurrentConsumers != null - && this.consumers.size() >= this.maxConcurrentConsumers) { + if (maxConcurrentConsumersToCheck != null + && this.consumers.size() >= maxConcurrentConsumersToCheck) { break; } BlockingQueueConsumer consumer = createBlockingQueueConsumer(); @@ -849,8 +859,10 @@ protected void addAndStartConsumers(int delta) { private void considerAddingAConsumer() { this.consumersLock.lock(); try { + Integer maxConcurrentConsumersToCheck = this.maxConcurrentConsumers; if (this.consumers != null - && this.maxConcurrentConsumers != null && this.consumers.size() < this.maxConcurrentConsumers) { + && maxConcurrentConsumersToCheck != null && this.consumers.size() < maxConcurrentConsumersToCheck) { + long now = System.currentTimeMillis(); if (this.lastConsumerStarted + this.startConsumerMinInterval < now) { this.addAndStartConsumers(1); @@ -918,7 +930,7 @@ protected BlockingQueueConsumer createBlockingQueueConsumer() { String[] queues = getQueueNames(); // There's no point prefetching less than the tx size, otherwise the consumer will stall because the broker // didn't get an ack for delivered messages - int actualPrefetchCount = getPrefetchCount() > this.batchSize ? getPrefetchCount() : this.batchSize; + int actualPrefetchCount = Math.max(getPrefetchCount(), this.batchSize); consumer = new BlockingQueueConsumer(getConnectionFactory(), getMessagePropertiesConverter(), this.cancellationLock, getAcknowledgeMode(), isChannelTransacted(), actualPrefetchCount, isDefaultRequeueRejected(), getConsumerArguments(), isNoLocal(), isExclusive(), queues); @@ -948,7 +960,8 @@ private void restart(BlockingQueueConsumer oldConsumer) { BlockingQueueConsumer consumer = oldConsumer; this.consumersLock.lock(); try { - if (this.consumers != null) { + Set consumersToUse = this.consumers; + if (consumersToUse != null) { try { // Need to recycle the channel in this consumer consumer.stop(); @@ -956,7 +969,7 @@ private void restart(BlockingQueueConsumer oldConsumer) { // to start because of the exception, but // we haven't counted down yet) this.cancellationLock.release(consumer); - this.consumers.remove(consumer); + consumersToUse.remove(consumer); if (!isActive()) { // Do not restart - container is stopping return; @@ -964,7 +977,7 @@ private void restart(BlockingQueueConsumer oldConsumer) { BlockingQueueConsumer newConsumer = createBlockingQueueConsumer(); newConsumer.setBackOffExecution(consumer.getBackOffExecution()); consumer = newConsumer; - this.consumers.add(consumer); + consumersToUse.add(consumer); if (getApplicationEventPublisher() != null) { getApplicationEventPublisher() .publishEvent(new AsyncConsumerRestartedEvent(this, oldConsumer, newConsumer)); @@ -984,6 +997,7 @@ private void restart(BlockingQueueConsumer oldConsumer) { } } + @SuppressWarnings("NullAway") // Dataflow analysis limitation private boolean receiveAndExecute(final BlockingQueueConsumer consumer) throws Exception { // NOSONAR PlatformTransactionManager transactionManager = getTransactionManager(); @@ -994,7 +1008,7 @@ private boolean receiveAndExecute(final BlockingQueueConsumer consumer) throws E new TransactionTemplate(transactionManager, getTransactionAttribute()); } return this.transactionTemplate - .execute(status -> { // NOSONAR null never returned + .execute(status -> { RabbitResourceHolder resourceHolder = ConnectionFactoryUtils.bindResourceToTransaction( new RabbitResourceHolder(consumer.getChannel(), false), getConnectionFactory(), true); @@ -1048,33 +1062,24 @@ private boolean doReceiveAndExecute(BlockingQueueConsumer consumer) throws Excep if (message == null) { break; } + MessageProperties messageProperties = message.getMessageProperties(); if (this.consumerBatchEnabled) { Collection afterReceivePostProcessors = getAfterReceivePostProcessors(); if (afterReceivePostProcessors != null) { - Message original = message; - deliveryTag = message.getMessageProperties().getDeliveryTag(); - for (MessagePostProcessor processor : getAfterReceivePostProcessors()) { + deliveryTag = messageProperties.getDeliveryTag(); + for (MessagePostProcessor processor : afterReceivePostProcessors) { message = processor.postProcessMessage(message); - if (message == null) { - if (this.logger.isDebugEnabled()) { - this.logger.debug( - "Message Post Processor returned 'null', discarding message " + original); - } - break; - } } } - if (message != null) { - if (messages == null) { - messages = new ArrayList<>(this.batchSize); - } - BatchingStrategy batchingStrategy = getBatchingStrategy(); - if (isDeBatchingEnabled() && batchingStrategy.canDebatch(message.getMessageProperties())) { - batchingStrategy.deBatch(message, messages::add); - } - else { - messages.add(message); - } + if (messages == null) { + messages = new ArrayList<>(this.batchSize); + } + BatchingStrategy batchingStrategy = getBatchingStrategy(); + if (isDeBatchingEnabled() && batchingStrategy.canDebatch(messageProperties)) { + batchingStrategy.deBatch(message, messages::add); + } + else { + messages.add(message); } } else { @@ -1089,7 +1094,7 @@ private boolean doReceiveAndExecute(BlockingQueueConsumer consumer) throws Excep if (this.logger.isDebugEnabled()) { this.logger.debug("User requested ack for failed delivery '" + e.getMessage() + "': " - + message.getMessageProperties().getDeliveryTag()); + + messageProperties.getDeliveryTag()); } immediateAck = this.enforceImmediateAckForManual; break; @@ -1098,18 +1103,19 @@ private boolean doReceiveAndExecute(BlockingQueueConsumer consumer) throws Excep if (causeChainHasImmediateAcknowledgeAmqpException(ex)) { if (this.logger.isDebugEnabled()) { this.logger.debug("User requested ack for failed delivery: " - + message.getMessageProperties().getDeliveryTag()); + + messageProperties.getDeliveryTag()); } immediateAck = this.enforceImmediateAckForManual; break; } long tagToRollback = isAsyncReplies() - ? message.getMessageProperties().getDeliveryTag() + ? messageProperties.getDeliveryTag() : -1; if (getTransactionManager() != null) { if (getTransactionAttribute().rollbackOn(ex)) { - RabbitResourceHolder resourceHolder = (RabbitResourceHolder) TransactionSynchronizationManager - .getResource(getConnectionFactory()); + RabbitResourceHolder resourceHolder = + (RabbitResourceHolder) TransactionSynchronizationManager.getResource( + getConnectionFactory()); if (resourceHolder != null) { consumer.clearDeliveryTags(); } @@ -1227,7 +1233,7 @@ protected void handleStartupFailure(BackOffExecution backOffExecution) { } @Override - protected void publishConsumerFailedEvent(String reason, boolean fatal, @Nullable Throwable t) { + protected void publishConsumerFailedEvent(@Nullable String reason, boolean fatal, @Nullable Throwable t) { if (!fatal || !isRunning()) { super.publishConsumerFailedEvent(reason, fatal, t); } @@ -1244,7 +1250,7 @@ protected void publishConsumerFailedEvent(String reason, boolean fatal, @Nullabl @Override public String toString() { return "SimpleMessageListenerContainer " - + (getBeanName() != null ? "(" + getBeanName() + ") " : "") + + "(" + getBeanName() + ") " + "[concurrentConsumers=" + this.concurrentConsumers + (this.maxConcurrentConsumers != null ? ", maxConcurrentConsumers=" + this.maxConcurrentConsumers : "") + ", queueNames=" + Arrays.toString(getQueueNames()) + "]"; @@ -1258,7 +1264,7 @@ private final class AsyncMessageProcessingConsumer implements Runnable { private final CountDownLatch start; - private volatile FatalListenerStartupException startupException; + private volatile @Nullable FatalListenerStartupException startupException; private int consecutiveIdles; @@ -1280,7 +1286,7 @@ private final class AsyncMessageProcessingConsumer implements Runnable { * @return a startup exception if there was one * @throws InterruptedException if the consumer startup is interrupted */ - private FatalListenerStartupException getStartupException() throws InterruptedException { + private @Nullable FatalListenerStartupException getStartupException() throws InterruptedException { if (!this.start.await( SimpleMessageListenerContainer.this.consumerStartTimeout, TimeUnit.MILLISECONDS)) { logger.error("Consumer failed to start in " @@ -1303,8 +1309,10 @@ public void run() { // NOSONAR - line count this.consumer.setLocallyTransacted(isChannelLocallyTransacted()); String routingLookupKey = getRoutingLookupKey(); + RoutingConnectionFactory routingConnectionFactoryToUse = getRoutingConnectionFactory(); if (routingLookupKey != null) { - SimpleResourceHolder.bind(getRoutingConnectionFactory(), routingLookupKey); // NOSONAR both never null + Assert.state(routingConnectionFactoryToUse != null, "'routingConnectionFactory' must be provided."); + SimpleResourceHolder.bind(routingConnectionFactoryToUse, routingLookupKey); // NOSONAR both never null } if (this.consumer.getQueueCount() < 1) { @@ -1379,8 +1387,14 @@ public void run() { // NOSONAR - line count catch (AmqpIOException e) { if (RabbitUtils.exclusiveAccesssRefused(e)) { this.failedExclusive = true; - getExclusiveConsumerExceptionLogger().log(logger, - "Exclusive consumer failure", e.getCause().getCause()); + Throwable cause = e.getCause(); + if (cause != null) { + cause = cause.getCause() == null ? cause : cause.getCause(); + } + else { + cause = e; + } + getExclusiveConsumerExceptionLogger().log(logger, "Exclusive consumer failure", cause); publishConsumerFailedEvent("Consumer raised exception, attempting restart", false, e); } else { @@ -1411,8 +1425,8 @@ public void run() { // NOSONAR - line count killOrRestart(aborted); - if (routingLookupKey != null) { - SimpleResourceHolder.unbind(getRoutingConnectionFactory()); // NOSONAR never null here + if (routingConnectionFactoryToUse != null) { + SimpleResourceHolder.unbind(routingConnectionFactoryToUse); } } @@ -1499,7 +1513,7 @@ private void initialize() throws Throwable { // NOSONAR throw ex; } else { - Throwable possibleAuthException = ex.getCause().getCause(); + Throwable possibleAuthException = findAuthException(ex); if (!(possibleAuthException instanceof PossibleAuthenticationFailureException)) { throw ex; } @@ -1525,6 +1539,17 @@ private void initialize() throws Throwable { // NOSONAR } } + private static Throwable findAuthException(Throwable ex) { + Throwable cause = ex.getCause(); + if (cause != null) { + cause = cause.getCause(); + if (cause != null) { + return cause; + } + } + return ex; + } + private void killOrRestart(boolean aborted) { if (!isActive(this.consumer) || aborted) { logger.debug("Cancelling " + this.consumer); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java index aa3e426cd5..f49266e05d 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java @@ -26,6 +26,7 @@ import com.rabbitmq.client.Channel; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpException; import org.springframework.amqp.core.AcknowledgeMode; @@ -88,35 +89,35 @@ public abstract class AbstractAdaptableMessageListener implements ChannelAwareMe private final StandardEvaluationContext evalContext = new StandardEvaluationContext(); + private final MessagePropertiesConverter messagePropertiesConverter = new DefaultMessagePropertiesConverter(); + private String responseRoutingKey = DEFAULT_RESPONSE_ROUTING_KEY; - private String responseExchange = null; + private @Nullable String responseExchange; - private Address responseAddress = null; + private @Nullable Address responseAddress; - private Expression responseExpression; + private @Nullable Expression responseExpression; private boolean mandatoryPublish; - private MessageConverter messageConverter = new SimpleMessageConverter(); - - private volatile MessagePropertiesConverter messagePropertiesConverter = new DefaultMessagePropertiesConverter(); + private @Nullable MessageConverter messageConverter = new SimpleMessageConverter(); private String encoding = DEFAULT_ENCODING; - private MessagePostProcessor[] beforeSendReplyPostProcessors; + private MessagePostProcessor @Nullable [] beforeSendReplyPostProcessors; - private RetryTemplate retryTemplate; + private @Nullable RetryTemplate retryTemplate; - private RecoveryCallback recoveryCallback; + private @Nullable RecoveryCallback recoveryCallback; private boolean isManualAck; private boolean defaultRequeueRejected = true; - private ReplyPostProcessor replyPostProcessor; + private @Nullable ReplyPostProcessor replyPostProcessor; - private String replyContentType; + private @Nullable String replyContentType; private boolean converterWinsContentType = true; @@ -200,7 +201,7 @@ public void setMandatoryPublish(boolean mandatoryPublish) { * The default converter is a {@link SimpleMessageConverter}, which is able to handle "text" content-types. * @param messageConverter The message converter. */ - public void setMessageConverter(MessageConverter messageConverter) { + public void setMessageConverter(@Nullable MessageConverter messageConverter) { this.messageConverter = messageConverter; } @@ -241,8 +242,10 @@ public void setRecoveryCallback(RecoveryCallback recoveryCallback) { * @param beanResolver the resolver. * @since 1.6 */ - public void setBeanResolver(BeanResolver beanResolver) { - this.evalContext.setBeanResolver(beanResolver); + public void setBeanResolver(@Nullable BeanResolver beanResolver) { + if (beanResolver != null) { + this.evalContext.setBeanResolver(beanResolver); + } this.evalContext.setTypeConverter(new StandardTypeConverter()); this.evalContext.addPropertyAccessor(new MapAccessor()); } @@ -263,7 +266,7 @@ public void setReplyPostProcessor(ReplyPostProcessor replyPostProcessor) { * @return the content type. * @since 2.3 */ - protected String getReplyContentType() { + protected @Nullable String getReplyContentType() { return this.replyContentType; } @@ -299,7 +302,7 @@ public void setConverterWinsContentType(boolean converterWinsContentType) { * returned from listener methods back to Rabbit messages. * @return The message converter. */ - protected MessageConverter getMessageConverter() { + protected @Nullable MessageConverter getMessageConverter() { return this.messageConverter; } @@ -345,10 +348,7 @@ protected void handleListenerException(Throwable ex) { */ protected Object extractMessage(Message message) { MessageConverter converter = getMessageConverter(); - if (converter != null) { - return converter.fromMessage(message); - } - return message; + return converter != null ? converter.fromMessage(message) : message; } /** @@ -356,13 +356,13 @@ protected Object extractMessage(Message message) { * response message back. * @param resultArg the result object to handle (never null) * @param request the original request message - * @param channel the Rabbit channel to operate on (may be null) + * @param channel the Rabbit channel to operate on (maybe null) * @see #buildMessage * @see #postProcessResponse * @see #getReplyToAddress(Message, Object, InvocationResult) * @see #sendResponse */ - protected void handleResult(InvocationResult resultArg, Message request, Channel channel) { + protected void handleResult(InvocationResult resultArg, Message request, @Nullable Channel channel) { handleResult(resultArg, request, channel, null); } @@ -379,21 +379,24 @@ protected void handleResult(InvocationResult resultArg, Message request, Channel * @see #getReplyToAddress(Message, Object, InvocationResult) * @see #sendResponse */ - protected void handleResult(InvocationResult resultArg, Message request, Channel channel, Object source) { - if (channel != null) { + @SuppressWarnings("NullAway") // Dataflow analysis limitation + protected void handleResult(@Nullable InvocationResult resultArg, Message request, + @Nullable Channel channel, @Nullable Object source) { + + if (channel != null && resultArg != null) { if (resultArg.getReturnValue() instanceof CompletableFuture completable) { if (!this.isManualAck) { this.logger.warn("Container AcknowledgeMode must be MANUAL for a Future return type; " + "otherwise the container will ack the message immediately"); } completable.whenComplete((r, t) -> { - if (t == null) { - asyncSuccess(resultArg, request, channel, source, r); - basicAck(request, channel); - } - else { - asyncFailure(request, channel, t, source); - } + if (t == null) { + asyncSuccess(resultArg, request, channel, source, r); + basicAck(request, channel); + } + else { + asyncFailure(request, channel, t, source); + } }); } else if (monoPresent && MonoHandler.isMono(resultArg.getReturnValue())) { @@ -416,8 +419,8 @@ else if (this.logger.isWarnEnabled()) { } } - private void asyncSuccess(InvocationResult resultArg, Message request, Channel channel, Object source, - Object deferredResult) { + private void asyncSuccess(InvocationResult resultArg, Message request, Channel channel, + @Nullable Object source, @Nullable Object deferredResult) { if (deferredResult == null) { this.logger.debug("Async result is null, ignoring"); @@ -444,16 +447,18 @@ private void asyncSuccess(InvocationResult resultArg, Message request, Channel c } } - protected void basicAck(Message request, Channel channel) { - try { - channel.basicAck(request.getMessageProperties().getDeliveryTag(), false); - } - catch (IOException e) { - this.logger.error("Failed to ack message", e); + protected void basicAck(Message request, @Nullable Channel channel) { + if (channel != null) { + try { + channel.basicAck(request.getMessageProperties().getDeliveryTag(), false); + } + catch (IOException e) { + this.logger.error("Failed to ack message", e); + } } } - protected void asyncFailure(Message request, Channel channel, Throwable t, Object source) { + protected void asyncFailure(Message request, Channel channel, Throwable t, @Nullable Object source) { this.logger.error("Future, Mono, or suspend function was completed with an exception for " + request, t); try { channel.basicNack(request.getMessageProperties().getDeliveryTag(), false, @@ -464,7 +469,9 @@ protected void asyncFailure(Message request, Channel channel, Throwable t, Objec } } - protected void doHandleResult(InvocationResult resultArg, Message request, Channel channel, Object source) { + protected void doHandleResult(InvocationResult resultArg, Message request, Channel channel, + @Nullable Object source) { + if (this.logger.isDebugEnabled()) { this.logger.debug("Listener method returned result [" + resultArg + "] - generating response message for it"); @@ -486,7 +493,7 @@ protected void doHandleResult(InvocationResult resultArg, Message request, Chann } } - protected String getReceivedExchange(Message request) { + protected @Nullable String getReceivedExchange(Message request) { return request.getMessageProperties().getReceivedExchange(); } @@ -498,7 +505,7 @@ protected String getReceivedExchange(Message request) { * @return the Rabbit Message (never null). * @see #setMessageConverter */ - protected Message buildMessage(Channel channel, Object result, Type genericType) { + protected Message buildMessage(Channel channel, @Nullable Object result, @Nullable Type genericType) { MessageConverter converter = getMessageConverter(); if (converter != null && !(result instanceof Message)) { return convert(result, genericType, converter); @@ -522,12 +529,18 @@ protected Message buildMessage(Channel channel, Object result, Type genericType) * @return the message. * @since 2.3 */ - protected Message convert(Object result, Type genericType, MessageConverter converter) { + protected Message convert(@Nullable Object result, @Nullable Type genericType, MessageConverter converter) { MessageProperties messageProperties = new MessageProperties(); if (this.replyContentType != null) { messageProperties.setContentType(this.replyContentType); } - Message message = converter.toMessage(result, messageProperties, genericType); + Message message; + if (result == null) { + message = new Message(new byte[0], messageProperties); + } + else { + message = converter.toMessage(result, messageProperties, genericType); + } if (this.replyContentType != null && !this.converterWinsContentType) { message.getMessageProperties().setContentType(this.replyContentType); } @@ -571,7 +584,7 @@ protected void postProcessResponse(Message request, Message response) { * @see org.springframework.amqp.core.Message#getMessageProperties() * @see org.springframework.amqp.core.MessageProperties#getReplyTo() */ - protected Address getReplyToAddress(Message request, Object source, InvocationResult result) { + protected Address getReplyToAddress(Message request, @Nullable Object source, InvocationResult result) { Address replyTo = request.getMessageProperties().getReplyToAddress(); if (replyTo == null) { if (this.responseAddress == null && this.responseExchange != null) { @@ -596,7 +609,9 @@ else if (this.responseAddress == null) { return replyTo; } - private Address evaluateReplyTo(Message request, Object source, Object result, Expression expression) { + private Address evaluateReplyTo(Message request, @Nullable Object source, @Nullable Object result, + Expression expression) { + Address replyTo; Object value = expression.getValue(this.evalContext, new ReplyExpressionRoot(request, source, result)); Assert.state(value instanceof String || value instanceof Address, @@ -666,7 +681,6 @@ protected void doPublish(Channel channel, Address replyTo, Message message) thro * Post-process the given message before sending the response. *

* The default implementation is empty. - * * @param channel The channel. * @param response the outgoing Rabbit message about to be sent */ @@ -680,11 +694,11 @@ public static final class ReplyExpressionRoot { private final Message request; - private final Object source; + private final @Nullable Object source; - private final Object result; + private final @Nullable Object result; - protected ReplyExpressionRoot(Message request, Object source, Object result) { + protected ReplyExpressionRoot(Message request, @Nullable Object source, @Nullable Object result) { this.request = request; this.source = source; this.result = result; @@ -694,11 +708,11 @@ public Message getRequest() { return this.request; } - public Object getSource() { + public @Nullable Object getSource() { return this.source; } - public Object getResult() { + public @Nullable Object getResult() { return this.result; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AmqpMessageHandlerMethodFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AmqpMessageHandlerMethodFactory.java index 4ac0263a5b..d73e4e8567 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AmqpMessageHandlerMethodFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AmqpMessageHandlerMethodFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2023-2025 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. @@ -22,9 +22,10 @@ import java.util.List; import java.util.Optional; +import org.jspecify.annotations.Nullable; + import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.converter.MessageConverter; import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory; @@ -49,9 +50,10 @@ public class AmqpMessageHandlerMethodFactory extends DefaultMessageHandlerMethod private final HandlerMethodArgumentResolverComposite argumentResolvers = new HandlerMethodArgumentResolverComposite(); + @SuppressWarnings("NullAway.Init") private MessageConverter messageConverter; - private Validator validator; + private @Nullable Validator validator; @Override public void setMessageConverter(MessageConverter messageConverter) { @@ -93,7 +95,7 @@ private static class OptionalEmptyAwarePayloadArgumentResolver extends PayloadMe } @Override - public Object resolveArgument(MethodParameter parameter, Message message) throws Exception { // NOSONAR + public @Nullable Object resolveArgument(MethodParameter parameter, Message message) throws Exception { // NOSONAR Object resolved; try { resolved = super.resolveArgument(parameter, message); @@ -134,7 +136,7 @@ private boolean isOptional(Message message, Type type) { } @Override - protected boolean isEmptyPayload(Object payload) { + protected boolean isEmptyPayload(@Nullable Object payload) { return payload == null || payload.equals(Optional.empty()); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapter.java index b3eb8bf968..ecb97f6016 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapter.java @@ -23,6 +23,7 @@ import java.util.concurrent.CompletableFuture; import com.rabbitmq.client.Channel; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.rabbit.batch.BatchingStrategy; import org.springframework.amqp.rabbit.batch.SimpleBatchingStrategy; @@ -31,10 +32,10 @@ import org.springframework.amqp.rabbit.listener.support.ContainerUtils; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.amqp.support.converter.MessageConversionException; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.support.GenericMessage; import org.springframework.messaging.support.MessageBuilder; +import org.springframework.util.Assert; /** * A listener adapter for batch listeners. @@ -52,8 +53,9 @@ public class BatchMessagingMessageListenerAdapter extends MessagingMessageListen private final BatchingStrategy batchingStrategy; - public BatchMessagingMessageListenerAdapter(Object bean, Method method, boolean returnExceptions, - RabbitListenerErrorHandler errorHandler, @Nullable BatchingStrategy batchingStrategy) { + @SuppressWarnings("this-escape") + public BatchMessagingMessageListenerAdapter(@Nullable Object bean, @Nullable Method method, boolean returnExceptions, + @Nullable RabbitListenerErrorHandler errorHandler, @Nullable BatchingStrategy batchingStrategy) { super(bean, method, returnExceptions, errorHandler, true); this.converterAdapter = (MessagingMessageConverterAdapter) getMessagingMessageConverter(); @@ -61,7 +63,7 @@ public BatchMessagingMessageListenerAdapter(Object bean, Method method, boolean } @Override - public void onMessageBatch(List messages, Channel channel) { + public void onMessageBatch(List messages, @Nullable Channel channel) { Message converted; if (this.converterAdapter.isAmqpMessageList()) { converted = new GenericMessage<>(messages); @@ -76,6 +78,7 @@ public void onMessageBatch(List messages, catch (MessageConversionException e) { this.logger.error("Could not convert incoming message", e); try { + Assert.notNull(channel, "'channel' cannot be null"); channel.basicReject(message.getMessageProperties().getDeliveryTag(), false); } catch (Exception ex) { @@ -104,12 +107,13 @@ public void onMessageBatch(List messages, } private void invokeHandlerAndProcessResult(List amqpMessages, - Channel channel, Message message) { + @Nullable Channel channel, Message message) { if (logger.isDebugEnabled()) { logger.debug("Processing [" + message + "]"); } - InvocationResult result = invokeHandler(null, channel, message); + InvocationResult result = invokeHandler(channel, message, true, + amqpMessages.toArray(new org.springframework.amqp.core.Message[0])); if (result.getReturnValue() != null) { handleResult(result, amqpMessages, channel); } @@ -118,8 +122,9 @@ private void invokeHandlerAndProcessResult(List amqpMessages, - Channel channel) { + @Nullable Channel channel) { if (channel != null) { if (resultArg.getReturnValue() instanceof CompletableFuture completable) { @@ -175,16 +180,13 @@ protected Message toMessagingMessage(org.springframework.amqp.core.Message am if (this.converterAdapter.isMessageList()) { List> messages = new ArrayList<>(); - this.batchingStrategy.deBatch(amqpMessage, fragment -> { - messages.add(super.toMessagingMessage(fragment)); - }); + this.batchingStrategy.deBatch(amqpMessage, fragment -> messages.add(super.toMessagingMessage(fragment))); return new GenericMessage<>(messages); } else { List list = new ArrayList<>(); - this.batchingStrategy.deBatch(amqpMessage, fragment -> { - list.add(this.converterAdapter.extractPayload(fragment)); - }); + this.batchingStrategy.deBatch(amqpMessage, fragment -> + list.add(this.converterAdapter.extractPayload(fragment))); return MessageBuilder.withPayload(list) .copyHeaders(this.converterAdapter .getHeaderMapper() diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandler.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandler.java index 4316d125af..dd416c0c03 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandler.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/DelegatingInvocableHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2023 the original author or authors. + * Copyright 2015-2025 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. @@ -20,13 +20,14 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.AmqpException; import org.springframework.amqp.support.AmqpHeaders; import org.springframework.beans.factory.config.BeanExpressionContext; @@ -37,7 +38,6 @@ import org.springframework.expression.ParserContext; import org.springframework.expression.common.TemplateParserContext; import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.converter.MessageConverter; @@ -49,7 +49,6 @@ import org.springframework.util.ReflectionUtils; import org.springframework.validation.Validator; - /** * Delegates to an {@link InvocableHandlerMethod} based on the message payload type. * Matches a single, non-annotated parameter or one that is annotated with @@ -75,17 +74,17 @@ public class DelegatingInvocableHandler { private final ConcurrentMap payloadMethodParameters = new ConcurrentHashMap<>(); - private final InvocableHandlerMethod defaultHandler; + private final @Nullable InvocableHandlerMethod defaultHandler; private final Map handlerSendTo = new ConcurrentHashMap<>(); private final Object bean; - private final BeanExpressionResolver resolver; + private final @Nullable BeanExpressionResolver resolver; - private final BeanExpressionContext beanExpressionContext; + private final @Nullable BeanExpressionContext beanExpressionContext; - private final PayloadValidator validator; + private final @Nullable PayloadValidator validator; private final boolean asyncReplies; @@ -114,6 +113,7 @@ public DelegatingInvocableHandler(List handlers, Object public DelegatingInvocableHandler(List handlers, @Nullable InvocableHandlerMethod defaultHandler, Object bean, BeanExpressionResolver beanExpressionResolver, BeanExpressionContext beanExpressionContext) { + this(handlers, defaultHandler, bean, beanExpressionResolver, beanExpressionContext, null); } @@ -128,8 +128,9 @@ public DelegatingInvocableHandler(List handlers, * @since 2.0.3 */ public DelegatingInvocableHandler(List handlers, - @Nullable InvocableHandlerMethod defaultHandler, Object bean, BeanExpressionResolver beanExpressionResolver, - BeanExpressionContext beanExpressionContext, @Nullable Validator validator) { + @Nullable InvocableHandlerMethod defaultHandler, Object bean, + @Nullable BeanExpressionResolver beanExpressionResolver, + @Nullable BeanExpressionContext beanExpressionContext, @Nullable Validator validator) { this.handlers = new ArrayList<>(handlers); this.defaultHandler = defaultHandler; @@ -139,9 +140,8 @@ public DelegatingInvocableHandler(List handlers, this.validator = validator == null ? null : new PayloadValidator(validator); boolean asyncRepl; asyncRepl = defaultHandler != null && isAsyncReply(defaultHandler); - Iterator iterator = handlers.iterator(); - while (iterator.hasNext()) { - asyncRepl |= isAsyncReply(iterator.next()); + for (InvocableHandlerMethod handler : handlers) { + asyncRepl |= isAsyncReply(handler); } this.asyncReplies = asyncRepl; } @@ -175,8 +175,8 @@ public boolean isAsyncReplies() { * @throws Exception raised if no suitable argument resolver can be found, * or the method raised an exception. */ - public InvocationResult invoke(Message message, Object... providedArgs) throws Exception { // NOSONAR - Class payloadClass = message.getPayload().getClass(); + public InvocationResult invoke(Message message, @Nullable Object... providedArgs) throws Exception { // NOSONAR + Class payloadClass = message.getPayload().getClass(); InvocableHandlerMethod handler = getHandlerForPayload(payloadClass); if (this.validator != null && this.defaultHandler != null) { MethodParameter parameter = this.payloadMethodParameters.get(handler); @@ -200,7 +200,8 @@ public InvocationResult invoke(Message message, Object... providedArgs) throw * @param payloadClass the payload class. * @return the handler. */ - protected InvocableHandlerMethod getHandlerForPayload(Class payloadClass) { + @SuppressWarnings("NullAway") // Dataflow analysis limitation + protected InvocableHandlerMethod getHandlerForPayload(Class payloadClass) { InvocableHandlerMethod handler = this.cachedHandlers.get(payloadClass); if (handler == null) { handler = findHandlerForPayload(payloadClass); @@ -209,22 +210,19 @@ protected InvocableHandlerMethod getHandlerForPayload(Class pa new NoSuchMethodException("No listener method found in " + this.bean.getClass().getName() + " for " + payloadClass)); } - this.cachedHandlers.putIfAbsent(payloadClass, handler); //NOSONAR + this.cachedHandlers.putIfAbsent(payloadClass, handler); setupReplyTo(handler); } return handler; } private void setupReplyTo(InvocableHandlerMethod handler) { - String replyTo = null; Method method = handler.getMethod(); - if (method != null) { - SendTo ann = AnnotationUtils.getAnnotation(method, SendTo.class); - replyTo = extractSendTo(method.toString(), ann); - } + SendTo ann = AnnotationUtils.getAnnotation(method, SendTo.class); + String replyTo = extractSendTo(method.toString(), ann); if (replyTo == null) { Class beanType = handler.getBeanType(); - SendTo ann = AnnotationUtils.getAnnotation(beanType, SendTo.class); + ann = AnnotationUtils.getAnnotation(beanType, SendTo.class); replyTo = extractSendTo(beanType.getSimpleName(), ann); } if (replyTo != null) { @@ -232,7 +230,7 @@ private void setupReplyTo(InvocableHandlerMethod handler) { } } - private String extractSendTo(String element, SendTo ann) { + private @Nullable String extractSendTo(String element, @Nullable SendTo ann) { String replyTo = null; if (ann != null) { String[] destinations = ann.value(); @@ -245,19 +243,20 @@ private String extractSendTo(String element, SendTo ann) { return replyTo; } - private String resolve(String value) { - if (this.resolver != null) { + private @Nullable String resolve(String value) { + if (this.beanExpressionContext != null) { String resolvedValue = this.beanExpressionContext.getBeanFactory().resolveEmbeddedValue(value); - Object newValue = this.resolver.evaluate(resolvedValue, this.beanExpressionContext); - Assert.isInstanceOf(String.class, newValue, "Invalid @SendTo expression"); - return (String) newValue; - } - else { - return value; + if (this.resolver != null) { + Object newValue = this.resolver.evaluate(resolvedValue, this.beanExpressionContext); + Assert.isInstanceOf(String.class, newValue, "Invalid @SendTo expression"); + return (String) newValue; + } } + + return value; } - protected InvocableHandlerMethod findHandlerForPayload(Class payloadClass) { + protected @Nullable InvocableHandlerMethod findHandlerForPayload(Class payloadClass) { InvocableHandlerMethod result = null; for (InvocableHandlerMethod handler : this.handlers) { if (matchHandlerMethod(payloadClass, handler)) { @@ -277,7 +276,7 @@ protected InvocableHandlerMethod findHandlerForPayload(Class p return result != null ? result : this.defaultHandler; } - protected boolean matchHandlerMethod(Class payloadClass, InvocableHandlerMethod handler) { + protected boolean matchHandlerMethod(Class payloadClass, InvocableHandlerMethod handler) { Method method = handler.getMethod(); Annotation[][] parameterAnnotations = method.getParameterAnnotations(); // Single param; no annotation or not @Header @@ -285,7 +284,7 @@ protected boolean matchHandlerMethod(Class payloadClass, Invoc MethodParameter methodParameter = new MethodParameter(method, 0); if ((methodParameter.getParameterAnnotations().length == 0 || !methodParameter.hasParameterAnnotation(Header.class)) - && methodParameter.getParameterType().isAssignableFrom(payloadClass)) { + && methodParameter.getParameterType().isAssignableFrom(payloadClass)) { if (this.validator != null) { this.payloadMethodParameters.put(handler, methodParameter); } @@ -295,7 +294,7 @@ protected boolean matchHandlerMethod(Class payloadClass, Invoc return findACandidate(payloadClass, handler, method, parameterAnnotations); } - private boolean findACandidate(Class payloadClass, InvocableHandlerMethod handler, Method method, + private boolean findACandidate(Class payloadClass, InvocableHandlerMethod handler, Method method, Annotation[][] parameterAnnotations) { boolean foundCandidate = false; @@ -303,7 +302,7 @@ private boolean findACandidate(Class payloadClass, InvocableHa MethodParameter methodParameter = new MethodParameter(method, i); if ((methodParameter.getParameterAnnotations().length == 0 || !methodParameter.hasParameterAnnotation(Header.class)) - && methodParameter.getParameterType().isAssignableFrom(payloadClass)) { + && methodParameter.getParameterType().isAssignableFrom(payloadClass)) { if (foundCandidate) { throw new AmqpException("Ambiguous payload parameter for " + method.toGenericString()); } @@ -346,8 +345,7 @@ public boolean hasDefaultHandler() { return this.defaultHandler != null; } - @Nullable - public InvocationResult getInvocationResultFor(Object result, Object inboundPayload) { + public @Nullable InvocationResult getInvocationResultFor(Object result, Object inboundPayload) { InvocableHandlerMethod handler = findHandlerForPayload(inboundPayload.getClass()); if (handler != null) { return new InvocationResult(result, this.handlerSendTo.get(handler), @@ -362,15 +360,12 @@ private static final class PayloadValidator extends PayloadMethodArgumentResolve super(new MessageConverter() { // Required but never used @Override - @Nullable - public Message toMessage(Object payload, @Nullable - MessageHeaders headers) { + public @Nullable Message toMessage(Object payload, @Nullable MessageHeaders headers) { return null; } @Override - @Nullable - public Object fromMessage(Message message, Class targetClass) { + public @Nullable Object fromMessage(Message message, Class targetClass) { return null; } @@ -383,4 +378,5 @@ public void validate(Message message, MethodParameter parameter, Object targe } } + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/HandlerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/HandlerAdapter.java index e6e0f1a172..d4fcf039a6 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/HandlerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/HandlerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2022 the original author or authors. + * Copyright 2015-2025 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. @@ -20,9 +20,11 @@ import java.lang.reflect.Type; import java.util.concurrent.CompletableFuture; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; +import org.springframework.util.Assert; /** * A wrapper for either an {@link InvocableHandlerMethod} or @@ -35,9 +37,9 @@ */ public class HandlerAdapter { - private final InvocableHandlerMethod invokerHandlerMethod; + private final @Nullable InvocableHandlerMethod invokerHandlerMethod; - private final DelegatingInvocableHandler delegatingHandler; + private final @Nullable DelegatingInvocableHandler delegatingHandler; private final boolean asyncReplies; @@ -70,14 +72,16 @@ public HandlerAdapter(DelegatingInvocableHandler delegatingHandler) { * @return the invocation result. * @throws Exception if one occurs. */ - public InvocationResult invoke(@Nullable Message message, Object... providedArgs) throws Exception { // NOSONAR - if (this.invokerHandlerMethod != null) { // NOSONAR (nullable message) - return new InvocationResult(this.invokerHandlerMethod.invoke(message, providedArgs), - null, this.invokerHandlerMethod.getMethod().getGenericReturnType(), - this.invokerHandlerMethod.getBean(), - this.invokerHandlerMethod.getMethod()); + public InvocationResult invoke(Message message, @Nullable Object... providedArgs) throws Exception { // NOSONAR + InvocableHandlerMethod invokerHandlerMethodToUse = this.invokerHandlerMethod; + if (invokerHandlerMethodToUse != null) { // NOSONAR (nullable message) + return new InvocationResult(invokerHandlerMethodToUse.invoke(message, providedArgs), + null, invokerHandlerMethodToUse.getMethod().getGenericReturnType(), + invokerHandlerMethodToUse.getBean(), + invokerHandlerMethodToUse.getMethod()); } - else if (this.delegatingHandler.hasDefaultHandler()) { + Assert.notNull(this.delegatingHandler, "'delegatingHandler' or 'invokerHandlerMethod' is required"); + if (this.delegatingHandler.hasDefaultHandler()) { // Needed to avoid returning raw Message which matches Object Object[] args = new Object[providedArgs.length + 1]; args[0] = message.getPayload(); @@ -99,6 +103,7 @@ public String getMethodAsString(Object payload) { return this.invokerHandlerMethod.getMethod().toGenericString(); } else { + Assert.notNull(this.delegatingHandler, "'delegatingHandler' or 'invokerHandlerMethod' is required"); return this.delegatingHandler.getMethodNameFor(payload); } } @@ -114,6 +119,7 @@ public Method getMethodFor(Object payload) { return this.invokerHandlerMethod.getMethod(); } else { + Assert.notNull(this.delegatingHandler, "'delegatingHandler' or 'invokerHandlerMethod' is required"); return this.delegatingHandler.getMethodFor(payload); } } @@ -129,6 +135,7 @@ public Type getReturnTypeFor(Object payload) { return this.invokerHandlerMethod.getMethod().getReturnType(); } else { + Assert.notNull(this.delegatingHandler, "'delegatingHandler' or 'invokerHandlerMethod' is required"); return this.delegatingHandler.getMethodFor(payload).getReturnType(); } } @@ -142,6 +149,7 @@ public Object getBean() { return this.invokerHandlerMethod.getBean(); } else { + Assert.notNull(this.delegatingHandler, "'delegatingHandler' or 'invokerHandlerMethod' is required"); return this.delegatingHandler.getBean(); } } @@ -162,13 +170,13 @@ public boolean isAsyncReplies() { * @return the invocation result. * @since 2.1.7 */ - @Nullable - public InvocationResult getInvocationResultFor(Object result, Object inboundPayload) { + public @Nullable InvocationResult getInvocationResultFor(Object result, Object inboundPayload) { if (this.invokerHandlerMethod != null) { return new InvocationResult(result, null, this.invokerHandlerMethod.getMethod().getGenericReturnType(), this.invokerHandlerMethod.getBean(), this.invokerHandlerMethod.getMethod()); } else { + Assert.notNull(this.delegatingHandler, "'delegatingHandler' or 'invokerHandlerMethod' is required"); return this.delegatingHandler.getInvocationResultFor(result, inboundPayload); } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/InvocationResult.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/InvocationResult.java index feb70f89a2..414955df82 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/InvocationResult.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/InvocationResult.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2020 the original author or authors. + * Copyright 2018-2025 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. @@ -19,30 +19,29 @@ import java.lang.reflect.Method; import java.lang.reflect.Type; +import org.jspecify.annotations.Nullable; + import org.springframework.expression.Expression; -import org.springframework.lang.Nullable; /** * The result of a listener method invocation. * * @author Gary Russell + * @author Artem Bilan * * @since 2.1 */ public final class InvocationResult { - private final Object returnValue; + private final @Nullable Object returnValue; - private final Expression sendTo; + private final @Nullable Expression sendTo; - @Nullable - private final Type returnType; + private final @Nullable Type returnType; - @Nullable - private final Object bean; + private final @Nullable Object bean; - @Nullable - private final Method method; + private final @Nullable Method method; /** * Construct an instance with the provided properties. @@ -52,7 +51,7 @@ public final class InvocationResult { * @param bean the bean. * @param method the method. */ - public InvocationResult(Object result, @Nullable Expression sendTo, @Nullable Type returnType, + public InvocationResult(@Nullable Object result, @Nullable Expression sendTo, @Nullable Type returnType, @Nullable Object bean, @Nullable Method method) { this.returnValue = result; @@ -62,11 +61,11 @@ public InvocationResult(Object result, @Nullable Expression sendTo, @Nullable Ty this.method = method; } - public Object getReturnValue() { + public @Nullable Object getReturnValue() { return this.returnValue; } - public Expression getSendTo() { + public @Nullable Expression getSendTo() { return this.sendTo; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/KotlinAwareInvocableHandlerMethod.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/KotlinAwareInvocableHandlerMethod.java index a3506e3315..fd7cba2603 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/KotlinAwareInvocableHandlerMethod.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/KotlinAwareInvocableHandlerMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2023-2025 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. @@ -18,6 +18,8 @@ import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; + import org.springframework.core.CoroutinesUtils; import org.springframework.core.KotlinDetector; import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; @@ -36,7 +38,7 @@ public KotlinAwareInvocableHandlerMethod(Object bean, Method method) { } @Override - protected Object doInvoke(Object... args) throws Exception { + protected @Nullable Object doInvoke(@Nullable Object... args) throws Exception { Method method = getBridgedMethod(); if (KotlinDetector.isSuspendingFunction(method)) { return CoroutinesUtils.invokeSuspendingFunction(method, getBean(), args); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapter.java index 26222b4529..e39f632d90 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapter.java @@ -23,9 +23,9 @@ import java.util.Map; import com.rabbitmq.client.Channel; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpIOException; -import org.springframework.amqp.AmqpIllegalStateException; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.core.MessageProperties; @@ -40,7 +40,7 @@ /** * Message listener adapter that delegates the handling of messages to target listener methods via reflection, with * flexible message type conversion. Allows listener methods to operate on message content types, completely independent - * from the Rabbit API. + * of the Rabbit API. * *

* By default, the content of incoming Rabbit messages gets extracted before being passed into the target listener @@ -67,6 +67,7 @@ * Message will be sent back as all of these methods return void. * *

+ * {@code
  * public interface MessageContentsDelegate {
  * 	void handleMessage(String text);
  *
@@ -76,15 +77,18 @@
  *
  * 	void handleMessage(Serializable obj);
  * }
+ * }
  * 
* * This next example handle a Message type and gets passed the actual (raw) Message as an * argument. Again, no Message will be sent back as all of these methods return void. * *
+ * {@code
  * public interface RawMessageDelegate {
  * 	void handleMessage(Message message);
  * }
+ * }
  * 
* * This next example illustrates a Message delegate that just consumes the String contents of @@ -93,9 +97,11 @@ * definition). Again, no Message will be sent back as the method returns void. * *
+ * {@code
  * public interface TextMessageContentDelegate {
  * 	void onMessage(String text);
  * }
+ * }
  * 
* * This final example illustrates a Message delegate that just consumes the String contents of @@ -103,9 +109,11 @@ * configured {@link MessageListenerAdapter} sending a {@link Message} in response. * *
+ * {@code
  * public interface ResponsiveTextMessageContentDelegate {
  * 	String handleMessage(String text);
  * }
+ * }
  * 
* * For further examples and discussion please do refer to the Spring reference documentation which describes this class @@ -119,6 +127,7 @@ * @author Greg Turnquist * @author Cai Kun * @author Ngoc Nhan + * @author Artem Bilan * * @see #setDelegate * @see #setDefaultListenerMethod @@ -137,12 +146,11 @@ public class MessageListenerAdapter extends AbstractAdaptableMessageListener { */ public static final String ORIGINAL_DEFAULT_LISTENER_METHOD = "handleMessage"; - + @SuppressWarnings("NullAway.Init") private Object delegate; private String defaultListenerMethod = ORIGINAL_DEFAULT_LISTENER_METHOD; - /** * Create a new {@link MessageListenerAdapter} with default settings. */ @@ -163,6 +171,7 @@ public MessageListenerAdapter(Object delegate) { * @param delegate the delegate object * @param messageConverter the message converter to use */ + @SuppressWarnings("this-escape") public MessageListenerAdapter(Object delegate, MessageConverter messageConverter) { doSetDelegate(delegate); super.setMessageConverter(messageConverter); @@ -219,7 +228,6 @@ protected String getDefaultListenerMethod() { return this.defaultListenerMethod; } - /** * Set the mapping of queue name or consumer tag to method name. The first lookup * is by queue name, if that returns null, we lookup by consumer tag, if that @@ -265,7 +273,7 @@ public String removeQueueOrTagToMethodName(String queueOrTag) { * @throws Exception if thrown by Rabbit API methods */ @Override - public void onMessage(Message message, Channel channel) throws Exception { // NOSONAR + public void onMessage(Message message, @Nullable Channel channel) throws Exception { // NOSONAR // Check whether the delegate is a MessageListener impl itself. // In that case, the adapter will simply act as a pass-through. Object delegateListener = getDelegate(); @@ -283,11 +291,6 @@ else if (delegateListener instanceof MessageListener messageListener) { // Regular case: find a handler method reflectively. Object convertedMessage = extractMessage(message); String methodName = getListenerMethodName(message, convertedMessage); - if (methodName == null) { - throw new AmqpIllegalStateException("No default listener method specified: " - + "Either specify a non-null value for the 'defaultListenerMethod' property or " - + "override the 'getListenerMethodName' method."); - } // Invoke the handler method with appropriate arguments. Object[] listenerArguments = buildListenerArguments(convertedMessage, channel, message); @@ -344,8 +347,8 @@ protected String getListenerMethodName(Message originalMessage, Object extracted * @return the array of arguments to be passed into the listener method (each element of the array corresponding to * a distinct method argument) */ - protected Object[] buildListenerArguments(Object extractedMessage, Channel channel, Message message) { - return new Object[] { extractedMessage }; + protected Object[] buildListenerArguments(Object extractedMessage, @Nullable Channel channel, Message message) { + return new Object[] {extractedMessage}; } /** @@ -357,12 +360,16 @@ protected Object[] buildListenerArguments(Object extractedMessage, Channel chann * @see #getListenerMethodName * @see #buildListenerArguments */ - protected Object invokeListenerMethod(String methodName, Object[] arguments, Message originalMessage) { + protected @Nullable Object invokeListenerMethod(String methodName, @Nullable Object @Nullable [] arguments, + Message originalMessage) { + try { MethodInvoker methodInvoker = new MethodInvoker(); methodInvoker.setTargetObject(getDelegate()); methodInvoker.setTargetMethod(methodName); - methodInvoker.setArguments(arguments); + if (arguments != null) { + methodInvoker.setArguments(arguments); + } methodInvoker.prepare(); return methodInvoker.invoke(); } @@ -380,7 +387,7 @@ protected Object invokeListenerMethod(String methodName, Object[] arguments, Mes ArrayList arrayClass = new ArrayList<>(); if (arguments != null) { for (Object argument : arguments) { - arrayClass.add(argument.getClass().toString()); + arrayClass.add(argument != null ? argument.getClass().toString() : " null"); } } throw new ListenerExecutionFailedException("Failed to invoke target method '" + methodName diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java index ba7b52821c..b6033b14bf 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java @@ -25,20 +25,22 @@ import java.util.Optional; import com.rabbitmq.client.Channel; +import org.jspecify.annotations.Nullable; +import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.rabbit.listener.api.RabbitListenerErrorHandler; import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; import org.springframework.amqp.support.AmqpHeaderMapper; -import org.springframework.amqp.support.converter.MessageConversionException; +import org.springframework.amqp.support.SimpleAmqpHeaderMapper; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.amqp.support.converter.MessagingMessageConverter; import org.springframework.amqp.support.converter.RemoteInvocationResult; import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.MessagingException; +import org.springframework.messaging.converter.MessageConversionException; import org.springframework.messaging.handler.annotation.Header; import org.springframework.messaging.handler.annotation.Headers; import org.springframework.messaging.handler.annotation.Payload; @@ -46,13 +48,13 @@ import org.springframework.util.TypeUtils; /** - * A {@link org.springframework.amqp.core.MessageListener MessageListener} + * A {@link MessageListener MessageListener} * adapter that invokes a configurable {@link HandlerAdapter}. * *

Wraps the incoming {@link org.springframework.amqp.core.Message * AMQP Message} to Spring's {@link Message} abstraction, copying the * standard headers using a configurable - * {@link org.springframework.amqp.support.AmqpHeaderMapper AmqpHeaderMapper}. + * {@link AmqpHeaderMapper AmqpHeaderMapper}. * *

The original {@link org.springframework.amqp.core.Message Message} and * the {@link Channel} are provided as additional arguments so that these can @@ -67,29 +69,30 @@ */ public class MessagingMessageListenerAdapter extends AbstractAdaptableMessageListener { - private HandlerAdapter handlerAdapter; - private final MessagingMessageConverterAdapter messagingMessageConverter; private final boolean returnExceptions; - private final RabbitListenerErrorHandler errorHandler; + private final @Nullable RabbitListenerErrorHandler errorHandler; + + private @Nullable HandlerAdapter handlerAdapter; public MessagingMessageListenerAdapter() { this(null, null); } - public MessagingMessageListenerAdapter(Object bean, Method method) { + public MessagingMessageListenerAdapter(@Nullable Object bean, @Nullable Method method) { this(bean, method, false, null); } - public MessagingMessageListenerAdapter(Object bean, Method method, boolean returnExceptions, - RabbitListenerErrorHandler errorHandler) { + public MessagingMessageListenerAdapter(@Nullable Object bean, @Nullable Method method, boolean returnExceptions, + @Nullable RabbitListenerErrorHandler errorHandler) { + this(bean, method, returnExceptions, errorHandler, false); } - protected MessagingMessageListenerAdapter(Object bean, Method method, boolean returnExceptions, - RabbitListenerErrorHandler errorHandler, boolean batch) { + protected MessagingMessageListenerAdapter(@Nullable Object bean, @Nullable Method method, boolean returnExceptions, + @Nullable RabbitListenerErrorHandler errorHandler, boolean batch) { this.messagingMessageConverter = new MessagingMessageConverterAdapter(bean, method, batch); this.returnExceptions = returnExceptions; @@ -106,20 +109,22 @@ public void setHandlerAdapter(HandlerAdapter handlerAdapter) { } protected HandlerAdapter getHandlerAdapter() { + Assert.notNull(this.handlerAdapter, "The 'handlerAdapter' is required"); return this.handlerAdapter; } @Override public boolean isAsyncReplies() { + Assert.notNull(this.handlerAdapter, "The 'handlerAdapter' is required"); return this.handlerAdapter.isAsyncReplies(); } /** * Set the {@link AmqpHeaderMapper} implementation to use to map the standard - * AMQP headers. By default, a {@link org.springframework.amqp.support.SimpleAmqpHeaderMapper + * AMQP headers. By default, a {@link SimpleAmqpHeaderMapper * SimpleAmqpHeaderMapper} is used. * @param headerMapper the {@link AmqpHeaderMapper} instance. - * @see org.springframework.amqp.support.SimpleAmqpHeaderMapper + * @see SimpleAmqpHeaderMapper */ public void setHeaderMapper(AmqpHeaderMapper headerMapper) { Assert.notNull(headerMapper, "HeaderMapper must not be null"); @@ -128,20 +133,24 @@ public void setHeaderMapper(AmqpHeaderMapper headerMapper) { /** * @return the {@link MessagingMessageConverter} for this listener, - * being able to convert {@link org.springframework.messaging.Message}. + * being able to convert {@link Message}. */ protected final MessagingMessageConverter getMessagingMessageConverter() { return this.messagingMessageConverter; } @Override - public void setMessageConverter(MessageConverter messageConverter) { + public void setMessageConverter(@Nullable MessageConverter messageConverter) { super.setMessageConverter(messageConverter); - this.messagingMessageConverter.setPayloadConverter(messageConverter); + if (messageConverter != null) { + this.messagingMessageConverter.setPayloadConverter(messageConverter); + } } @Override - public void onMessage(org.springframework.amqp.core.Message amqpMessage, Channel channel) throws Exception { // NOSONAR + public void onMessage(org.springframework.amqp.core.Message amqpMessage, @Nullable Channel channel) + throws Exception { + Message message = null; try { message = toMessagingMessage(amqpMessage); @@ -161,7 +170,7 @@ public void onMessage(org.springframework.amqp.core.Message amqpMessage, Channel @Override protected void asyncFailure(org.springframework.amqp.core.Message request, Channel channel, Throwable t, - Object source) { + @Nullable Object source) { try { handleException(request, channel, (Message) source, @@ -169,11 +178,12 @@ protected void asyncFailure(org.springframework.amqp.core.Message request, Chann return; } catch (Exception ex) { + // Ignore } super.asyncFailure(request, channel, t, source); } - private void handleException(org.springframework.amqp.core.Message amqpMessage, Channel channel, + private void handleException(org.springframework.amqp.core.Message amqpMessage, @Nullable Channel channel, @Nullable Message message, ListenerExecutionFailedException e) throws Exception { // NOSONAR if (this.errorHandler != null) { @@ -183,7 +193,7 @@ private void handleException(org.springframework.amqp.core.Message amqpMessage, Object payload = message == null ? null : message.getPayload(); InvocationResult invResult = payload == null ? new InvocationResult(errorResult, null, null, null, null) - : this.handlerAdapter.getInvocationResultFor(errorResult, payload); + : getHandlerAdapter().getInvocationResultFor(errorResult, payload); handleResult(invResult, amqpMessage, channel, message); } else { @@ -199,22 +209,22 @@ private void handleException(org.springframework.amqp.core.Message amqpMessage, } } - protected void invokeHandlerAndProcessResult(@Nullable org.springframework.amqp.core.Message amqpMessage, - Channel channel, Message message) throws Exception { // NOSONAR + protected void invokeHandlerAndProcessResult(org.springframework.amqp.core.Message amqpMessage, + @Nullable Channel channel, Message message) { - boolean projectionUsed = amqpMessage == null ? false : amqpMessage.getMessageProperties().isProjectionUsed(); + boolean projectionUsed = amqpMessage.getMessageProperties().isProjectionUsed(); if (projectionUsed) { amqpMessage.getMessageProperties().setProjectionUsed(false); } if (logger.isDebugEnabled() && !projectionUsed) { logger.debug("Processing [" + message + "]"); } - InvocationResult result = null; - if (this.messagingMessageConverter.method == null && amqpMessage != null) { + InvocationResult result; + if (this.messagingMessageConverter.method == null) { amqpMessage.getMessageProperties() - .setTargetMethod(this.handlerAdapter.getMethodFor(message.getPayload())); + .setTargetMethod(getHandlerAdapter().getMethodFor(message.getPayload())); } - result = invokeHandler(amqpMessage, channel, message); + result = invokeHandler(channel, message, false, amqpMessage); if (result.getReturnValue() != null) { handleResult(result, amqpMessage, channel, message); } @@ -223,8 +233,8 @@ protected void invokeHandlerAndProcessResult(@Nullable org.springframework.amqp. } } - private void returnOrThrow(org.springframework.amqp.core.Message amqpMessage, Channel channel, Message message, - Throwable throwableToReturn, Exception exceptionToThrow) throws Exception { // NOSONAR + private void returnOrThrow(org.springframework.amqp.core.Message amqpMessage, @Nullable Channel channel, + @Nullable Message message, @Nullable Throwable throwableToReturn, Exception exceptionToThrow) throws Exception { if (!this.returnExceptions) { throw exceptionToThrow; @@ -232,13 +242,13 @@ private void returnOrThrow(org.springframework.amqp.core.Message amqpMessage, Ch Object payload = message == null ? null : message.getPayload(); try { handleResult(new InvocationResult(new RemoteInvocationResult(throwableToReturn), null, - payload == null ? Object.class : this.handlerAdapter.getReturnTypeFor(payload), - this.handlerAdapter.getBean(), - payload == null ? null : this.handlerAdapter.getMethodFor(payload)), + payload == null ? Object.class : getHandlerAdapter().getReturnTypeFor(payload), + getHandlerAdapter().getBean(), + payload == null ? null : getHandlerAdapter().getMethodFor(payload)), amqpMessage, channel, message); } catch (ReplyFailureException rfe) { - if (payload == null || void.class.equals(this.handlerAdapter.getReturnTypeFor(payload))) { + if (payload == null || void.class.equals(getHandlerAdapter().getReturnTypeFor(payload))) { throw exceptionToThrow; } else { @@ -254,37 +264,38 @@ protected Message toMessagingMessage(org.springframework.amqp.core.Message am /** * Invoke the handler, wrapping any exception to a {@link ListenerExecutionFailedException} * with a dedicated error message. - * @param amqpMessage the raw message. + * @param amqpMessages the raw AMQP messages. * @param channel the channel. * @param message the messaging message. * @return the result of invoking the handler. */ - protected InvocationResult invokeHandler(@Nullable org.springframework.amqp.core.Message amqpMessage, - Channel channel, Message message) { + protected InvocationResult invokeHandler(@Nullable Channel channel, Message message, + boolean batch, org.springframework.amqp.core.Message... amqpMessages) { try { - if (amqpMessage == null) { - return this.handlerAdapter.invoke(message, channel); + if (batch) { + return getHandlerAdapter().invoke(message, channel); } else { - return this.handlerAdapter.invoke(message, amqpMessage, channel, amqpMessage.getMessageProperties()); + org.springframework.amqp.core.Message amqpMessage = amqpMessages[0]; + return getHandlerAdapter().invoke(message, amqpMessage, channel, amqpMessage.getMessageProperties()); } } catch (MessagingException ex) { - throw new ListenerExecutionFailedException(createMessagingErrorMessage("Listener method could not " + - "be invoked with the incoming message", message.getPayload()), ex, amqpMessage); + throw new ListenerExecutionFailedException(createMessagingErrorMessage(message.getPayload()), + ex, amqpMessages); } catch (Exception ex) { throw new ListenerExecutionFailedException("Listener method '" + - this.handlerAdapter.getMethodAsString(message.getPayload()) + "' threw exception", ex, amqpMessage); + getHandlerAdapter().getMethodAsString(message.getPayload()) + "' threw exception", ex, amqpMessages); } } - private String createMessagingErrorMessage(String description, Object payload) { - return description + "\n" + private String createMessagingErrorMessage(Object payload) { + return "Listener method could not be invoked with the incoming message" + "\n" + "Endpoint handler details:\n" - + "Method [" + this.handlerAdapter.getMethodAsString(payload) + "]\n" - + "Bean [" + this.handlerAdapter.getBean() + "]"; + + "Method [" + getHandlerAdapter().getMethodAsString(payload) + "]\n" + + "Bean [" + getHandlerAdapter().getBean() + "]"; } /** @@ -296,7 +307,9 @@ private String createMessagingErrorMessage(String description, Object payload) { * @see #setMessageConverter */ @Override - protected org.springframework.amqp.core.Message buildMessage(Channel channel, Object result, Type genericType) { + protected org.springframework.amqp.core.Message buildMessage(Channel channel, @Nullable Object result, + @Nullable Type genericType) { + MessageConverter converter = getMessageConverter(); if (converter != null && !(result instanceof org.springframework.amqp.core.Message)) { if (result instanceof org.springframework.messaging.Message) { @@ -327,11 +340,11 @@ protected org.springframework.amqp.core.Message buildMessage(Channel channel, Ob */ protected final class MessagingMessageConverterAdapter extends MessagingMessageConverter { - private final Object bean; + private final @Nullable Object bean; - final Method method; // NOSONAR visibility + final @Nullable Method method; // NOSONAR visibility - private final Type inferredArgumentType; + private final @Nullable Type inferredArgumentType; private final boolean isBatch; @@ -341,13 +354,13 @@ protected final class MessagingMessageConverterAdapter extends MessagingMessageC private boolean isCollection; - MessagingMessageConverterAdapter(Object bean, Method method, boolean batch) { + MessagingMessageConverterAdapter(@Nullable Object bean, @Nullable Method method, boolean batch) { this.bean = bean; this.method = method; this.isBatch = batch; this.inferredArgumentType = determineInferredType(); if (logger.isDebugEnabled() && this.inferredArgumentType != null) { - logger.debug("Inferred argument type for " + method.toString() + " is " + this.inferredArgumentType); + logger.debug("Inferred argument type for " + method + " is " + this.inferredArgumentType); } } @@ -359,7 +372,7 @@ protected boolean isAmqpMessageList() { return this.isAmqpMessageList; } - protected Method getMethod() { + protected @Nullable Method getMethod() { return this.method; } @@ -378,7 +391,7 @@ protected Object extractPayload(org.springframework.amqp.core.Message message) { return extractMessage(message); } - private Type determineInferredType() { // NOSONAR - complexity + private @Nullable Type determineInferredType() { // NOSONAR - complexity if (this.method == null) { return null; } @@ -400,8 +413,7 @@ private Type determineInferredType() { // NOSONAR - complexity + ": Cannot annotate a parameter with both @Header and @Payload; " + "ignored for payload conversion"); } - if (isEligibleParameter(methodParameter) // NOSONAR - && (!isHeaderOrHeaders || isPayload) && !(isHeaderOrHeaders && isPayload)) { + if (isEligibleParameter(methodParameter) && !isHeaderOrHeaders) { if (genericParameterType == null) { genericParameterType = extractGenericParameterTypFromMethodParameter(methodParameter); @@ -425,7 +437,7 @@ private Type determineInferredType() { // NOSONAR - complexity return checkOptional(genericParameterType); } - protected Type checkOptional(Type genericParameterType) { + protected @Nullable Type checkOptional(@Nullable Type genericParameterType) { if (genericParameterType instanceof ParameterizedType pType && pType.getRawType().equals(Optional.class)) { return pType.getActualTypeArguments()[0]; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MonoHandler.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MonoHandler.java index 2030970803..4293a64c86 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MonoHandler.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MonoHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 the original author or authors. + * Copyright 2021-2025 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. @@ -18,11 +18,14 @@ import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; /** * Class to prevent direct links to {@link Mono}. + * * @author Gary Russell + * * @since 2.2.21 */ final class MonoHandler { // NOSONAR - pointless to name it ..Utils|Helper @@ -30,7 +33,7 @@ final class MonoHandler { // NOSONAR - pointless to name it ..Utils|Helper private MonoHandler() { } - static boolean isMono(Object result) { + static boolean isMono(@Nullable Object result) { return result instanceof Mono; } @@ -39,7 +42,7 @@ static boolean isMono(Class resultType) { } @SuppressWarnings("unchecked") - static void subscribe(Object returnValue, Consumer success, + static void subscribe(Object returnValue, @Nullable Consumer success, Consumer failure, Runnable completeConsumer) { ((Mono) returnValue).subscribe(success, failure, completeConsumer); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/package-info.java index 09c7c25b90..6322c6ef95 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/package-info.java @@ -1,4 +1,5 @@ /** * Provides classes for adapting listeners. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.listener.adapter; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/ChannelAwareBatchMessageListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/ChannelAwareBatchMessageListener.java index 990593943e..468ab9b752 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/ChannelAwareBatchMessageListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/ChannelAwareBatchMessageListener.java @@ -19,6 +19,7 @@ import java.util.List; import com.rabbitmq.client.Channel; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.Message; @@ -32,11 +33,11 @@ public interface ChannelAwareBatchMessageListener extends ChannelAwareMessageListener { @Override - default void onMessage(Message message, Channel channel) throws Exception { + default void onMessage(Message message, @Nullable Channel channel) throws Exception { throw new UnsupportedOperationException("Should never be called by the container"); } @Override - void onMessageBatch(List messages, Channel channel); + void onMessageBatch(List messages, @Nullable Channel channel); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/ChannelAwareMessageListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/ChannelAwareMessageListener.java index a5ad115df5..6f38e2535b 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/ChannelAwareMessageListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/ChannelAwareMessageListener.java @@ -19,10 +19,10 @@ import java.util.List; import com.rabbitmq.client.Channel; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageListener; -import org.springframework.lang.Nullable; /** * A message listener that is aware of the Channel on which the message was received. @@ -50,7 +50,7 @@ default void onMessage(Message message) { } @SuppressWarnings("unused") - default void onMessageBatch(List messages, Channel channel) { + default void onMessageBatch(List messages, @Nullable Channel channel) { throw new UnsupportedOperationException("This listener does not support message batches"); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/RabbitListenerErrorHandler.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/RabbitListenerErrorHandler.java index 4d9e800f75..3c4112f6c6 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/RabbitListenerErrorHandler.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/RabbitListenerErrorHandler.java @@ -17,10 +17,11 @@ package org.springframework.amqp.rabbit.listener.api; import com.rabbitmq.client.Channel; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; -import org.springframework.lang.Nullable; + /** * An error handler which is called when a {code @RabbitListener} method * throws an exception. This is invoked higher up the stack than the @@ -47,8 +48,9 @@ public interface RabbitListenerErrorHandler { * @throws Exception an exception which may be the original or different. * @since 3.1.3 */ - Object handleError(Message amqpMessage, Channel channel, - @Nullable org.springframework.messaging.Message message, + @Nullable + Object handleError(Message amqpMessage, @Nullable Channel channel, + org.springframework.messaging.@Nullable Message message, ListenerExecutionFailedException exception) throws Exception; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/package-info.java index c25b7c53fe..7ec1bd84de 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/api/package-info.java @@ -1,4 +1,5 @@ /** * Provides Additional APIs for listeners. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.listener.api; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/exception/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/exception/package-info.java index c6351b3ec2..b796af83b4 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/exception/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/exception/package-info.java @@ -1,4 +1,5 @@ /** * Provides classes for listener exceptions. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.listener.exception; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/package-info.java index c06b50e425..979ee9415a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/package-info.java @@ -1,5 +1,5 @@ /** * Provides classes for message listener containers. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.listener; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/support/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/support/package-info.java index 3f0670c263..bc14995a9c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/support/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/support/package-info.java @@ -1,4 +1,5 @@ /** * Provides support classes for listeners. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.listener.support; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j2/AmqpAppender.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j2/AmqpAppender.java index 4f2a19fccc..f56449bfd0 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j2/AmqpAppender.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j2/AmqpAppender.java @@ -54,6 +54,7 @@ import org.apache.logging.log4j.core.config.plugins.PluginElement; import org.apache.logging.log4j.core.layout.PatternLayout; import org.apache.logging.log4j.core.util.Integers; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpApplicationContextClosedException; import org.springframework.amqp.AmqpException; @@ -152,8 +153,8 @@ public class AmqpAppender extends AbstractAppender { * @param eventQueue the event queue. * @param properties the properties. */ - public AmqpAppender(String name, Filter filter, Layout layout, boolean ignoreExceptions, - Property[] properties, AmqpManager manager, BlockingQueue eventQueue) { + public AmqpAppender(String name, @Nullable Filter filter, Layout layout, + boolean ignoreExceptions, Property[] properties, AmqpManager manager, BlockingQueue eventQueue) { super(name, filter, layout, ignoreExceptions, properties); this.manager = manager; @@ -227,7 +228,7 @@ protected void sendEvent(Event event, Map properties) { } // Set applicationId, if we're using one - if (null != this.manager.applicationId) { + if (this.manager.applicationId != null) { amqpProps.setAppId(this.manager.applicationId); } @@ -266,14 +267,11 @@ protected void doSend(Event event, LogEvent logEvent, MessageProperties amqpProp this.layoutMutex.unlock(); } Message message = null; - if (this.manager.charset != null) { - try { - message = new Message(msgBody.toString().getBytes(this.manager.charset), - amqpProps); - } - catch (UnsupportedEncodingException e) { - /* fall back to default */ - } + try { + message = new Message(msgBody.toString().getBytes(this.manager.charset), amqpProps); + } + catch (UnsupportedEncodingException e) { + /* fall back to default */ } if (message == null) { message = new Message(msgBody.toString().getBytes(), amqpProps); //NOSONAR (default charset) @@ -345,7 +343,7 @@ public void run() { /** * Helper class to encapsulate a LoggingEvent, its MDC properties, and the number of retries. */ - protected static class Event { + public static class Event { private final LogEvent event; @@ -375,7 +373,7 @@ public int incrementRetries() { /** * Manager class for the appender. */ - protected static class AmqpManager extends AbstractManager { + public static class AmqpManager extends AbstractManager { private static final int DEFAULT_MAX_SENDER_RETRIES = 30; @@ -404,12 +402,13 @@ protected static class AmqpManager extends AbstractManager { /** * Log4J Layout to use to generate routing key. */ + @SuppressWarnings("NullAway.Init") private Layout routingKeyLayout; /** * Configuration arbitrary application ID. */ - private String applicationId = null; + private @Nullable String applicationId; /** * How many senders to use at once. Use more senders if you have lots of log output going through this appender. @@ -424,42 +423,43 @@ protected static class AmqpManager extends AbstractManager { /** * RabbitMQ ConnectionFactory. */ + @SuppressWarnings("NullAway.Init") private AbstractConnectionFactory connectionFactory; /** * RabbitMQ host to connect to. */ - private URI uri; + private @Nullable URI uri; /** * RabbitMQ host to connect to. */ - private String host; + private @Nullable String host; /** * A comma-delimited list of broker addresses: host:port[,host:port]*. */ - private String addresses; + private @Nullable String addresses; /** * RabbitMQ virtual host to connect to. */ - private String virtualHost; + private @Nullable String virtualHost; /** * RabbitMQ port to connect to. */ - private Integer port; + private @Nullable Integer port; /** * RabbitMQ user to connect as. */ - private String username; + private @Nullable String username; /** * RabbitMQ password for this user. */ - private String password; + private @Nullable String password; /** * Use an SSL connection. @@ -471,22 +471,22 @@ protected static class AmqpManager extends AbstractManager { /** * The SSL algorithm to use. */ - private String sslAlgorithm; + private @Nullable String sslAlgorithm; /** * Location of resource containing keystore and truststore information. */ - private String sslPropertiesLocation; + private @Nullable String sslPropertiesLocation; /** * Keystore location. */ - private String keyStore; + private @Nullable String keyStore; /** * Keystore passphrase. */ - private String keyStorePassphrase; + private @Nullable String keyStorePassphrase; /** * Keystore type. @@ -496,12 +496,12 @@ protected static class AmqpManager extends AbstractManager { /** * Truststore location. */ - private String trustStore; + private @Nullable String trustStore; /** * Truststore passphrase. */ - private String trustStorePassphrase; + private @Nullable String trustStorePassphrase; /** * Truststore type. @@ -512,7 +512,7 @@ protected static class AmqpManager extends AbstractManager { * SaslConfig. * @see RabbitUtils#stringToSaslConfig(String, ConnectionFactory) */ - private String saslConfig; + private @Nullable String saslConfig; /** * Default content-type of log messages. @@ -522,23 +522,23 @@ protected static class AmqpManager extends AbstractManager { /** * Default content-encoding of log messages. */ - private String contentEncoding = null; + private @Nullable String contentEncoding; /** - * Whether or not to try and declare the configured exchange when this appender starts. + * Whether to try and declare the configured exchange when this appender starts. */ private boolean declareExchange = false; /** * A name for the connection (appears on the RabbitMQ Admin UI). */ - private String connectionName; + private @Nullable String connectionName; /** * Additional client connection properties to be added to the rabbit connection, * with the form {@code key:value[,key:value]...}. */ - private String clientConnectionProperties; + private @Nullable String clientConnectionProperties; /** * charset to use when converting String to byte[], default null (system default charset used). @@ -548,7 +548,7 @@ protected static class AmqpManager extends AbstractManager { private String charset = Charset.defaultCharset().name(); /** - * Whether or not add MDC properties into message headers. true by default for backward compatibility + * Whether add MDC properties into message headers. true by default for backward compatibility */ private boolean addMdcAsHeaders = true; @@ -566,7 +566,8 @@ protected static class AmqpManager extends AbstractManager { /** * The pool of senders. */ - private ExecutorService senderPool = null; + @SuppressWarnings("NullAway.Init") + private ExecutorService senderPool; /** * Retries are delayed like: N ^ log(N), where N is the retry number. @@ -577,6 +578,7 @@ protected AmqpManager(LoggerContext loggerContext, String name) { super(loggerContext, name); } + @SuppressWarnings("NullAway") // Dataflow analysis limitation private boolean activateOptions() { ConnectionFactory rabbitConnectionFactory = createRabbitConnectionFactory(); if (rabbitConnectionFactory != null) { @@ -595,10 +597,8 @@ private boolean activateOptions() { if (this.addresses != null) { this.connectionFactory.setAddresses(this.addresses); } - if (this.clientConnectionProperties != null) { - ConnectionFactoryConfigurationUtils.updateClientConnectionProperties(this.connectionFactory, + ConnectionFactoryConfigurationUtils.updateClientConnectionProperties(this.connectionFactory, this.clientConnectionProperties); - } setUpExchangeDeclaration(); this.senderPool = Executors.newCachedThreadPool(); return true; @@ -608,10 +608,9 @@ private boolean activateOptions() { /** * Create the {@link ConnectionFactory}. - * * @return a {@link ConnectionFactory}. */ - protected ConnectionFactory createRabbitConnectionFactory() { + protected @Nullable ConnectionFactory createRabbitConnectionFactory() { RabbitConnectionFactoryBean factoryBean = new RabbitConnectionFactoryBean(); configureRabbitConnectionFactory(factoryBean); try { @@ -691,65 +690,56 @@ protected boolean releaseSub(long timeout, TimeUnit timeUnit) { protected void setUpExchangeDeclaration() { RabbitAdmin admin = new RabbitAdmin(this.connectionFactory); if (this.declareExchange) { - Exchange x; - if ("topic".equals(this.exchangeType)) { - x = new TopicExchange(this.exchangeName, this.durable, this.autoDelete); - } - else if ("direct".equals(this.exchangeType)) { - x = new DirectExchange(this.exchangeName, this.durable, this.autoDelete); - } - else if ("fanout".equals(this.exchangeType)) { - x = new FanoutExchange(this.exchangeName, this.durable, this.autoDelete); - } - else if ("headers".equals(this.exchangeType)) { - x = new HeadersExchange(this.exchangeName, this.durable, this.autoDelete); - } - else { - x = new TopicExchange(this.exchangeName, this.durable, this.autoDelete); - } + Exchange x = switch (this.exchangeType) { + case "direct" -> new DirectExchange(this.exchangeName, this.durable, this.autoDelete); + case "fanout" -> new FanoutExchange(this.exchangeName, this.durable, this.autoDelete); + case "headers" -> new HeadersExchange(this.exchangeName, this.durable, this.autoDelete); + default -> new TopicExchange(this.exchangeName, this.durable, this.autoDelete); + }; this.connectionFactory.addConnectionListener(new DeclareExchangeConnectionListener(x, admin)); } } } - protected static class Builder implements org.apache.logging.log4j.core.util.Builder { + public static class Builder implements org.apache.logging.log4j.core.util.Builder { @PluginConfiguration + @SuppressWarnings("NullAway.Init") private Configuration configuration; @PluginBuilderAttribute("name") - private String name; + private @Nullable String name; @PluginElement("Layout") - private Layout layout; + private @Nullable Layout layout; @PluginElement("Filter") - private Filter filter; + private @Nullable Filter filter; @PluginBuilderAttribute("ignoreExceptions") private boolean ignoreExceptions; @PluginBuilderAttribute("uri") - private URI uri; + private @Nullable URI uri; @PluginBuilderAttribute("host") - private String host; + private @Nullable String host; @PluginBuilderAttribute("port") - private String port; + private @Nullable String port; @PluginBuilderAttribute("addresses") - private String addresses; + private @Nullable String addresses; @PluginBuilderAttribute("user") - private String user; + private @Nullable String user; @PluginBuilderAttribute("password") - private String password; + private @Nullable String password; @PluginBuilderAttribute("virtualHost") - private String virtualHost; + private @Nullable String virtualHost; @PluginBuilderAttribute("useSsl") private boolean useSsl; @@ -758,31 +748,31 @@ protected static class Builder implements org.apache.logging.log4j.core.util.Bui private boolean verifyHostname; @PluginBuilderAttribute("sslAlgorithm") - private String sslAlgorithm; + private @Nullable String sslAlgorithm; @PluginBuilderAttribute("sslPropertiesLocation") - private String sslPropertiesLocation; + private @Nullable String sslPropertiesLocation; @PluginBuilderAttribute("keyStore") - private String keyStore; + private @Nullable String keyStore; @PluginBuilderAttribute("keyStorePassphrase") - private String keyStorePassphrase; + private @Nullable String keyStorePassphrase; @PluginBuilderAttribute("keyStoreType") - private String keyStoreType; + private @Nullable String keyStoreType; @PluginBuilderAttribute("trustStore") - private String trustStore; + private @Nullable String trustStore; @PluginBuilderAttribute("trustStorePassphrase") - private String trustStorePassphrase; + private @Nullable String trustStorePassphrase; @PluginBuilderAttribute("trustStoreType") - private String trustStoreType; + private @Nullable String trustStoreType; @PluginBuilderAttribute("saslConfig") - private String saslConfig; + private @Nullable String saslConfig; @PluginBuilderAttribute("senderPoolSize") private int senderPoolSize; @@ -791,22 +781,22 @@ protected static class Builder implements org.apache.logging.log4j.core.util.Bui private int maxSenderRetries; @PluginBuilderAttribute("applicationId") - private String applicationId; + private @Nullable String applicationId; @PluginBuilderAttribute("routingKeyPattern") - private String routingKeyPattern; + private @Nullable String routingKeyPattern; @PluginBuilderAttribute("generateId") private boolean generateId; @PluginBuilderAttribute("deliveryMode") - private String deliveryMode; + private @Nullable String deliveryMode; @PluginBuilderAttribute("exchange") - private String exchange; + private @Nullable String exchange; @PluginBuilderAttribute("exchangeType") - private String exchangeType; + private @Nullable String exchangeType; @PluginBuilderAttribute("declareExchange") private boolean declareExchange; @@ -818,28 +808,28 @@ protected static class Builder implements org.apache.logging.log4j.core.util.Bui private boolean autoDelete; @PluginBuilderAttribute("contentType") - private String contentType; + private @Nullable String contentType; @PluginBuilderAttribute("contentEncoding") - private String contentEncoding; + private @Nullable String contentEncoding; @PluginBuilderAttribute("connectionName") - private String connectionName; + private @Nullable String connectionName; @PluginBuilderAttribute("clientConnectionProperties") - private String clientConnectionProperties; + private @Nullable String clientConnectionProperties; @PluginBuilderAttribute("async") private boolean async; @PluginBuilderAttribute("charset") - private String charset; + private @Nullable String charset; @PluginBuilderAttribute("bufferSize") private int bufferSize = Integer.MAX_VALUE; @PluginElement(BlockingQueueFactory.ELEMENT_TYPE) - private BlockingQueueFactory blockingQueueFactory; + private @Nullable BlockingQueueFactory blockingQueueFactory; @PluginBuilderAttribute("addMdcAsHeaders") private boolean addMdcAsHeaders = Boolean.TRUE; @@ -1060,7 +1050,8 @@ public Builder setAddMdcAsHeaders(boolean addMdcAsHeaders) { } @Override - public AmqpAppender build() { + @SuppressWarnings("NullAway") // Dataflow analysis limitation + public @Nullable AmqpAppender build() { if (this.name == null) { LOGGER.error("No name for AmqpAppender"); } @@ -1093,7 +1084,8 @@ public AmqpAppender build() { .acceptIfNotNull(this.applicationId, value -> manager.applicationId = value) .acceptIfNotNull(this.routingKeyPattern, value -> manager.routingKeyPattern = value) .acceptIfNotNull(this.generateId, value -> manager.generateId = value) - .acceptIfNotNull(this.deliveryMode, value -> manager.deliveryMode = MessageDeliveryMode.valueOf(this.deliveryMode)) + .acceptIfNotNull(this.deliveryMode, + value -> manager.deliveryMode = MessageDeliveryMode.valueOf(this.deliveryMode)) .acceptIfNotNull(this.exchange, value -> manager.exchangeName = value) .acceptIfNotNull(this.exchangeType, value -> manager.exchangeType = value) .acceptIfNotNull(this.declareExchange, value -> manager.declareExchange = value) @@ -1115,7 +1107,8 @@ public AmqpAppender build() { eventQueue = this.blockingQueueFactory.create(this.bufferSize); } - AmqpAppender appender = buildInstance(this.name, this.filter, theLayout, this.ignoreExceptions, manager, eventQueue); + AmqpAppender appender = + buildInstance(this.name, this.filter, theLayout, this.ignoreExceptions, manager, eventQueue); if (manager.activateOptions()) { appender.startSenders(); return appender; @@ -1124,7 +1117,7 @@ public AmqpAppender build() { } /** - * Subclasses can extends Builder, use same logic but need to modify class instance. + * Subclasses can extend Builder, use same logic but need to modify class instance. * * @param name The Appender name. * @param filter The Filter to associate with the Appender. @@ -1135,7 +1128,7 @@ public AmqpAppender build() { * @param eventQueue Where LoggingEvents are queued to send. * @return {@link AmqpAppender} */ - protected AmqpAppender buildInstance(String name, Filter filter, Layout layout, + protected AmqpAppender buildInstance(String name, @Nullable Filter filter, Layout layout, boolean ignoreExceptions, AmqpManager manager, BlockingQueue eventQueue) { return new AmqpAppender(name, filter, layout, ignoreExceptions, Property.EMPTY_ARRAY, manager, eventQueue); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j2/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j2/package-info.java index e8fa832998..b68d9af973 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j2/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/log4j2/package-info.java @@ -1,4 +1,5 @@ /** * Provides classes supporting Log4j 2 appenders. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.log4j2; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/AmqpAppender.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/AmqpAppender.java index 8f662afcd6..f7a6616b6f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/AmqpAppender.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/AmqpAppender.java @@ -41,6 +41,7 @@ import ch.qos.logback.core.Layout; import ch.qos.logback.core.encoder.Encoder; import com.rabbitmq.client.ConnectionFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpApplicationContextClosedException; import org.springframework.amqp.AmqpException; @@ -157,17 +158,18 @@ public class AmqpAppender extends AppenderBase { /** * Configuration arbitrary application ID. */ - private String applicationId = null; + private @Nullable String applicationId = null; /** * Where LoggingEvents are queued to send. */ + @SuppressWarnings("NullAway.Init") private BlockingQueue events; /** * The pool of senders. */ - private ExecutorService senderPool = null; + private @Nullable ExecutorService senderPool; /** * How many senders to use at once. Use more senders if you have lots of log output going through this appender. @@ -187,55 +189,56 @@ public class AmqpAppender extends AppenderBase { /** * RabbitMQ ConnectionFactory. */ + @SuppressWarnings("NullAway.Init") private AbstractConnectionFactory connectionFactory; /** * A name for the connection (appears on the RabbitMQ Admin UI). */ - private String connectionName; + private @Nullable String connectionName; /** * Additional client connection properties added to the rabbit connection, with the form * {@code key:value[,key:value]...}. */ - private String clientConnectionProperties; + private @Nullable String clientConnectionProperties; /** * A comma-delimited list of broker addresses: host:port[,host:port]* * * @since 1.5.6 */ - private String addresses; + private @Nullable String addresses; /** * RabbitMQ host to connect to. */ - private URI uri; + private @Nullable URI uri; /** * RabbitMQ host to connect to. */ - private String host; + private @Nullable String host; /** * RabbitMQ virtual host to connect to. */ - private String virtualHost; + private @Nullable String virtualHost; /** * RabbitMQ port to connect to. */ - private Integer port; + private @Nullable Integer port; /** * RabbitMQ user to connect as. */ - private String username; + private @Nullable String username; /** * RabbitMQ password for this user. */ - private String password; + private @Nullable String password; /** * Use an SSL connection. @@ -245,22 +248,22 @@ public class AmqpAppender extends AppenderBase { /** * The SSL algorithm to use. */ - private String sslAlgorithm; + private @Nullable String sslAlgorithm; /** * Location of resource containing keystore and truststore information. */ - private String sslPropertiesLocation; + private @Nullable String sslPropertiesLocation; /** * Keystore location. */ - private String keyStore; + private @Nullable String keyStore; /** * Keystore passphrase. */ - private String keyStorePassphrase; + private @Nullable String keyStorePassphrase; /** * Keystore type. @@ -270,12 +273,12 @@ public class AmqpAppender extends AppenderBase { /** * Truststore location. */ - private String trustStore; + private @Nullable String trustStore; /** * Truststore passphrase. */ - private String trustStorePassphrase; + private @Nullable String trustStorePassphrase; /** * Truststore type. @@ -286,7 +289,7 @@ public class AmqpAppender extends AppenderBase { * SaslConfig. * @see RabbitUtils#stringToSaslConfig(String, ConnectionFactory) */ - private String saslConfig; + private @Nullable String saslConfig; private boolean verifyHostname = true; @@ -298,10 +301,10 @@ public class AmqpAppender extends AppenderBase { /** * Default content-encoding of log messages. */ - private String contentEncoding = null; + private @Nullable String contentEncoding = null; /** - * Whether or not to try and declare the configured exchange when this appender starts. + * Whether to try and declare the configured exchange when this appender starts. */ private boolean declareExchange = false; @@ -310,10 +313,10 @@ public class AmqpAppender extends AppenderBase { * If the charset is unsupported on the current platform, we fall back to using * the system charset. */ - private String charset; + private @Nullable String charset; /** - * Whether or not add MDC properties into message headers. true by default for backward compatibility + * Whether add MDC properties into message headers. true by default for backward compatibility */ private boolean addMdcAsHeaders = true; @@ -328,11 +331,11 @@ public class AmqpAppender extends AppenderBase { */ private boolean generateId = false; - private Layout layout; + private @Nullable Layout layout; - private Encoder encoder; + private @Nullable Encoder encoder; - private TargetLengthBasedClassNameAbbreviator abbreviator; + private @Nullable TargetLengthBasedClassNameAbbreviator abbreviator; private boolean includeCallerData; @@ -340,7 +343,7 @@ public void setRoutingKeyPattern(String routingKeyPattern) { this.routingKeyLayout.setPattern("%nopex{}" + routingKeyPattern); } - public URI getUri() { + public @Nullable URI getUri() { return this.uri; } @@ -348,7 +351,7 @@ public void setUri(URI uri) { this.uri = uri; } - public String getHost() { + public @Nullable String getHost() { return this.host; } @@ -356,7 +359,7 @@ public void setHost(String host) { this.host = host; } - public Integer getPort() { + public @Nullable Integer getPort() { return this.port; } @@ -368,11 +371,11 @@ public void setAddresses(String addresses) { this.addresses = addresses; } - public String getAddresses() { + public @Nullable String getAddresses() { return this.addresses; } - public String getVirtualHost() { + public @Nullable String getVirtualHost() { return this.virtualHost; } @@ -380,7 +383,7 @@ public void setVirtualHost(String virtualHost) { this.virtualHost = virtualHost; } - public String getUsername() { + public @Nullable String getUsername() { return this.username; } @@ -388,7 +391,7 @@ public void setUsername(String username) { this.username = username; } - public String getPassword() { + public @Nullable String getPassword() { return this.password; } @@ -423,7 +426,7 @@ public boolean isVerifyHostname() { return this.verifyHostname; } - public String getSslAlgorithm() { + public @Nullable String getSslAlgorithm() { return this.sslAlgorithm; } @@ -431,7 +434,7 @@ public void setSslAlgorithm(String sslAlgorithm) { this.sslAlgorithm = sslAlgorithm; } - public String getSslPropertiesLocation() { + public @Nullable String getSslPropertiesLocation() { return this.sslPropertiesLocation; } @@ -439,7 +442,7 @@ public void setSslPropertiesLocation(String sslPropertiesLocation) { this.sslPropertiesLocation = sslPropertiesLocation; } - public String getKeyStore() { + public @Nullable String getKeyStore() { return this.keyStore; } @@ -447,7 +450,7 @@ public void setKeyStore(String keyStore) { this.keyStore = keyStore; } - public String getKeyStorePassphrase() { + public @Nullable String getKeyStorePassphrase() { return this.keyStorePassphrase; } @@ -463,7 +466,7 @@ public void setKeyStoreType(String keyStoreType) { this.keyStoreType = keyStoreType; } - public String getTrustStore() { + public @Nullable String getTrustStore() { return this.trustStore; } @@ -471,7 +474,7 @@ public void setTrustStore(String trustStore) { this.trustStore = trustStore; } - public String getTrustStorePassphrase() { + public @Nullable String getTrustStorePassphrase() { return this.trustStorePassphrase; } @@ -487,7 +490,7 @@ public void setTrustStoreType(String trustStoreType) { this.trustStoreType = trustStoreType; } - public String getSaslConfig() { + public @Nullable String getSaslConfig() { return this.saslConfig; } @@ -537,7 +540,7 @@ public void setContentType(String contentType) { this.contentType = contentType; } - public String getContentEncoding() { + public @Nullable String getContentEncoding() { return this.contentEncoding; } @@ -545,7 +548,7 @@ public void setContentEncoding(String contentEncoding) { this.contentEncoding = contentEncoding; } - public String getApplicationId() { + public @Nullable String getApplicationId() { return this.applicationId; } @@ -609,7 +612,7 @@ public void setGenerateId(boolean generateId) { this.generateId = generateId; } - public String getCharset() { + public @Nullable String getCharset() { return this.charset; } @@ -621,7 +624,7 @@ public void setLayout(Layout layout) { this.layout = layout; } - public Encoder getEncoder() { + public @Nullable Encoder getEncoder() { return this.encoder; } @@ -666,7 +669,7 @@ public boolean isIncludeCallerData() { /** * If true, the caller data will be available in the target AMQP message. - * By default no caller data is sent to the RabbitMQ. + * By default, no caller data is sent to the RabbitMQ. * @param includeCallerData include or on caller data * @since 1.7.1 * @see ILoggingEvent#getCallerData() @@ -676,14 +679,17 @@ public void setIncludeCallerData(boolean includeCallerData) { } @Override + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void start() { this.events = createEventQueue(); ConnectionFactory rabbitConnectionFactory = createRabbitConnectionFactory(); if (rabbitConnectionFactory != null) { super.start(); - this.routingKeyLayout.setPattern(this.routingKeyLayout.getPattern() - .replaceAll("%property\\{applicationId}", this.applicationId)); + if (this.applicationId != null) { + this.routingKeyLayout.setPattern(this.routingKeyLayout.getPattern() + .replaceAll("%property\\{applicationId}", this.applicationId)); + } this.routingKeyLayout.setContext(getContext()); this.routingKeyLayout.start(); this.locationLayout.setContext(getContext()); @@ -700,10 +706,11 @@ public void start() { this.clientConnectionProperties); updateConnectionClientProperties(this.connectionFactory.getRabbitConnectionFactory().getClientProperties()); setUpExchangeDeclaration(); - this.senderPool = Executors.newCachedThreadPool(); + ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < this.senderPoolSize; i++) { - this.senderPool.submit(new EventSender()); + executorService.submit(new EventSender()); } + this.senderPool = executorService; } } @@ -711,7 +718,7 @@ public void start() { * Create the {@link ConnectionFactory}. * @return a {@link ConnectionFactory}. */ - protected ConnectionFactory createRabbitConnectionFactory() { + protected @Nullable ConnectionFactory createRabbitConnectionFactory() { RabbitConnectionFactoryBean factoryBean = new RabbitConnectionFactoryBean(); configureRabbitConnectionFactory(factoryBean); try { @@ -725,7 +732,7 @@ protected ConnectionFactory createRabbitConnectionFactory() { } /** - * Configure the {@link RabbitConnectionFactoryBean}. Sub-classes may override to + * Configure the {@link RabbitConnectionFactoryBean}. Subclasses may override to * customize the configuration of the bean. * @param factoryBean the {@link RabbitConnectionFactoryBean}. */ @@ -799,14 +806,12 @@ protected BlockingQueue createEventQueue() { @Override public void stop() { super.stop(); - if (null != this.senderPool) { + if (this.senderPool != null) { this.senderPool.shutdownNow(); this.senderPool = null; } - if (null != this.connectionFactory) { - this.connectionFactory.destroy(); - this.connectionFactory.onApplicationEvent(new ContextClosedEvent(this.context)); - } + this.connectionFactory.destroy(); + this.connectionFactory.onApplicationEvent(new ContextClosedEvent(this.context)); this.retryTimer.cancel(); this.routingKeyLayout.stop(); } @@ -823,22 +828,12 @@ protected void append(ILoggingEvent event) { protected void setUpExchangeDeclaration() { RabbitAdmin admin = new RabbitAdmin(this.connectionFactory); if (this.declareExchange) { - Exchange x; - if ("topic".equals(this.exchangeType)) { - x = new TopicExchange(this.exchangeName, this.durable, this.autoDelete); - } - else if ("direct".equals(this.exchangeType)) { - x = new DirectExchange(this.exchangeName, this.durable, this.autoDelete); - } - else if ("fanout".equals(this.exchangeType)) { - x = new FanoutExchange(this.exchangeName, this.durable, this.autoDelete); - } - else if ("headers".equals(this.exchangeType)) { - x = new HeadersExchange(this.exchangeType, this.durable, this.autoDelete); - } - else { - x = new TopicExchange(this.exchangeName, this.durable, this.autoDelete); - } + Exchange x = switch (this.exchangeType) { + case "direct" -> new DirectExchange(this.exchangeName, this.durable, this.autoDelete); + case "fanout" -> new FanoutExchange(this.exchangeName, this.durable, this.autoDelete); + case "headers" -> new HeadersExchange(this.exchangeType, this.durable, this.autoDelete); + default -> new TopicExchange(this.exchangeName, this.durable, this.autoDelete); + }; this.connectionFactory.addConnectionListener(new DeclareExchangeConnectionListener(x, admin)); } } @@ -996,6 +991,7 @@ private byte[] encodeMessage(ILoggingEvent logEvent) { return AmqpAppender.this.encoder.encode(logEvent); } + @SuppressWarnings("NullAway") // Dataflow analysis limitation String msgBody = AmqpAppender.this.layout.doLayout(logEvent); if (AmqpAppender.this.charset != null) { try { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/package-info.java index 6bb676d12f..e63351d0fe 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/logback/package-info.java @@ -1,4 +1,5 @@ /** * Provides classes supporting Logback appenders. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.logback; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/package-info.java index 9880b9bcef..8fa710bac5 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/package-info.java @@ -1,4 +1,5 @@ /** * Provides top-level classes for Spring Rabbit. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecoverer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecoverer.java index 61bb2122c7..d7ed3d3726 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecoverer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecoverer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2024 the original author or authors. + * Copyright 2014-2025 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. @@ -22,6 +22,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.AmqpTemplate; import org.springframework.amqp.core.Message; @@ -29,11 +30,10 @@ import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.rabbit.connection.RabbitUtils; import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.support.ValueExpression; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; -import org.springframework.expression.common.LiteralExpression; import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -72,9 +72,9 @@ public class RepublishMessageRecoverer implements MessageRecoverer { protected final AmqpTemplate errorTemplate; // NOSONAR - protected final Expression errorRoutingKeyExpression; // NOSONAR + protected final Expression errorRoutingKeyExpression; - protected final Expression errorExchangeNameExpression; // NOSONAR + protected final Expression errorExchangeNameExpression; protected final EvaluationContext evaluationContext = new StandardEvaluationContext(); @@ -113,7 +113,7 @@ public RepublishMessageRecoverer(AmqpTemplate errorTemplate, String errorExchang public RepublishMessageRecoverer(AmqpTemplate errorTemplate, @Nullable String errorExchange, @Nullable String errorRoutingKey) { - this(errorTemplate, new LiteralExpression(errorExchange), new LiteralExpression(errorRoutingKey)); // NOSONAR + this(errorTemplate, new ValueExpression<>(errorExchange), new ValueExpression<>(errorRoutingKey)); } /** @@ -128,8 +128,8 @@ public RepublishMessageRecoverer(AmqpTemplate errorTemplate, @Nullable Expressio Assert.notNull(errorTemplate, "'errorTemplate' cannot be null"); this.errorTemplate = errorTemplate; - this.errorExchangeNameExpression = errorExchange != null ? errorExchange : new LiteralExpression(null); // NOSONAR - this.errorRoutingKeyExpression = errorRoutingKey != null ? errorRoutingKey : new LiteralExpression(null); // NOSONAR + this.errorExchangeNameExpression = errorExchange != null ? errorExchange : new ValueExpression<>(null); + this.errorRoutingKeyExpression = errorRoutingKey != null ? errorRoutingKey : new ValueExpression<>(null); if (!(this.errorTemplate instanceof RabbitTemplate)) { this.maxStackTraceLength = Integer.MAX_VALUE; } @@ -191,9 +191,9 @@ protected MessageDeliveryMode getDeliveryMode() { @Override public void recover(Message message, Throwable cause) { MessageProperties messageProperties = message.getMessageProperties(); - Map headers = messageProperties.getHeaders(); + Map headers = messageProperties.getHeaders(); String exceptionMessage = cause.getCause() != null ? cause.getCause().getMessage() : cause.getMessage(); - String[] processed = processStackTrace(cause, exceptionMessage); + @Nullable String[] processed = processStackTrace(cause, exceptionMessage); String stackTraceAsString = processed[0]; String truncatedExceptionMessage = processed[1]; if (truncatedExceptionMessage != null) { @@ -247,7 +247,7 @@ protected void doSend(@Nullable String exchange, String routingKey, Message mess } } - private String[] processStackTrace(Throwable cause, String exceptionMessage) { + private @Nullable String[] processStackTrace(Throwable cause, @Nullable String exceptionMessage) { String stackTraceAsString = getStackTraceAsString(cause); if (this.maxStackTraceLength < 0) { int maxStackTraceLen = RabbitUtils @@ -260,7 +260,7 @@ private String[] processStackTrace(Throwable cause, String exceptionMessage) { return truncateIfNecessary(cause, exceptionMessage, stackTraceAsString); } - private String[] truncateIfNecessary(Throwable cause, String exception, String stackTrace) { + private @Nullable String[] truncateIfNecessary(Throwable cause, @Nullable String exception, String stackTrace) { boolean truncated = false; String stackTraceAsString = stackTrace; String exceptionMessage = exception == null ? "" : exception; @@ -295,7 +295,7 @@ else if (stackTraceAsString.length() + exceptionMessage.length() > this.maxStack } } } - return new String[] { stackTraceAsString, truncated ? truncatedExceptionMessage : null }; + return new @Nullable String[] {stackTraceAsString, truncated ? truncatedExceptionMessage : null}; } /** @@ -304,7 +304,7 @@ else if (stackTraceAsString.length() + exceptionMessage.length() > this.maxStack * @param cause The cause. * @return A {@link Map} of additional headers to add. */ - protected Map additionalHeaders(Message message, Throwable cause) { + protected @Nullable Map additionalHeaders(Message message, Throwable cause) { return null; } @@ -323,7 +323,7 @@ protected String prefixedOriginalRoutingKey(Message message) { * Create a String representation of the stack trace. * @param cause the throwable. * @return the String. - * @since 2.4.8 + * @since 2.4.8 */ protected String getStackTraceAsString(Throwable cause) { StringWriter stringWriter = new StringWriter(); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirms.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirms.java index 40909ecd69..0f9f79a32e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirms.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererWithConfirms.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2024 the original author or authors. + * Copyright 2021-2025 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. @@ -20,6 +20,8 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.AmqpMessageReturnedException; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory.ConfirmType; @@ -29,7 +31,6 @@ import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.expression.Expression; -import org.springframework.lang.Nullable; /** * A {@link RepublishMessageRecoverer} supporting publisher confirms and returns. @@ -85,7 +86,7 @@ public RepublishMessageRecovererWithConfirms(RabbitTemplate errorTemplate, Strin * @param errorRoutingKey the routing key. */ public RepublishMessageRecovererWithConfirms(RabbitTemplate errorTemplate, String errorExchange, - String errorRoutingKey, ConfirmType confirmType) { + @Nullable String errorRoutingKey, ConfirmType confirmType) { super(errorTemplate, errorExchange, errorRoutingKey); this.template = errorTemplate; @@ -128,7 +129,7 @@ protected void doSend(@Nullable String exchange, String routingKey, Message mess } } - private void doSendCorrelated(String exchange, String routingKey, Message message) { + private void doSendCorrelated(@Nullable String exchange, String routingKey, Message message) { CorrelationData cd = new CorrelationData(); if (exchange != null) { this.template.send(exchange, routingKey, message, cd); @@ -141,7 +142,7 @@ private void doSendCorrelated(String exchange, String routingKey, Message messag if (cd.getReturned() != null) { throw new AmqpMessageReturnedException("Message returned", cd.getReturned()); } - if (!confirm.isAck()) { + if (!confirm.ack()) { throw new AmqpNackReceivedException("Negative acknowledgment received", message); } } @@ -157,7 +158,7 @@ private void doSendCorrelated(String exchange, String routingKey, Message messag } } - private void doSendSimple(String exchange, String routingKey, Message message) { + private void doSendSimple(@Nullable String exchange, String routingKey, Message message) { this.template.invoke(sender -> { if (exchange != null) { sender.send(exchange, routingKey, message); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/package-info.java index 6346485ee4..4948f5de8e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/package-info.java @@ -1,4 +1,5 @@ /** * Provides classes supporting retries. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.retry; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ConsumerCancelledException.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ConsumerCancelledException.java index 1ad1a4c9a6..b5e9b74fcb 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ConsumerCancelledException.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ConsumerCancelledException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -16,6 +16,8 @@ package org.springframework.amqp.rabbit.support; +import java.io.Serial; + /** * Thrown when the broker cancels the consumer and the message * queue is drained. @@ -26,6 +28,7 @@ */ public class ConsumerCancelledException extends RuntimeException { + @Serial private static final long serialVersionUID = 3815997920289066359L; public ConsumerCancelledException() { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java index f9f4d9cf40..0d9e3cfd58 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java @@ -27,10 +27,10 @@ import com.rabbitmq.client.AMQP.BasicProperties; import com.rabbitmq.client.Envelope; import com.rabbitmq.client.LongString; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.MessageDeliveryMode; import org.springframework.amqp.core.MessageProperties; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -88,6 +88,7 @@ public DefaultMessagePropertiesConverter(int longStringLimit, boolean convertLon } @Override + @SuppressWarnings("NullAway") // Dataflow analysis limitation public MessageProperties toMessageProperties(BasicProperties source, @Nullable Envelope envelope, String charset) { MessageProperties target = new MessageProperties(); Map headers = source.getHeaders(); @@ -156,22 +157,22 @@ else if (MessageProperties.RETRY_COUNT.equals(key)) { @Override public BasicProperties fromMessageProperties(final MessageProperties source, final String charset) { BasicProperties.Builder target = new BasicProperties.Builder(); - Map headers = convertHeadersIfNecessary(source); + Map headers = convertHeadersIfNecessary(source); target.headers(headers) - .timestamp(source.getTimestamp()) - .messageId(source.getMessageId()) - .userId(source.getUserId()) - .appId(source.getAppId()) - .clusterId(source.getClusterId()) - .type(source.getType()); + .timestamp(source.getTimestamp()) + .messageId(source.getMessageId()) + .userId(source.getUserId()) + .appId(source.getAppId()) + .clusterId(source.getClusterId()) + .type(source.getType()); MessageDeliveryMode deliveryMode = source.getDeliveryMode(); if (deliveryMode != null) { target.deliveryMode(MessageDeliveryMode.toInt(deliveryMode)); } target.expiration(source.getExpiration()) - .priority(source.getPriority()) - .contentType(source.getContentType()) - .contentEncoding(source.getContentEncoding()); + .priority(source.getPriority()) + .contentType(source.getContentType()) + .contentEncoding(source.getContentEncoding()); String correlationId = source.getCorrelationId(); if (StringUtils.hasText(correlationId)) { target.correlationId(correlationId); @@ -183,16 +184,16 @@ public BasicProperties fromMessageProperties(final MessageProperties source, fin return target.build(); } - private Map convertHeadersIfNecessary(MessageProperties source) { - Map headers = source.getHeaders(); + private Map convertHeadersIfNecessary(MessageProperties source) { + Map headers = source.getHeaders(); long retryCount = source.getRetryCount(); - if (CollectionUtils.isEmpty(headers) && retryCount == 0) { - return Collections.emptyMap(); + if (headers.isEmpty() && retryCount == 0) { + return Collections.emptyMap(); } - Map writableHeaders = new HashMap<>(); + Map writableHeaders = new HashMap<>(); for (Map.Entry entry : headers.entrySet()) { - writableHeaders.put(entry.getKey(), this.convertHeaderValueIfNecessary(entry.getValue())); + writableHeaders.put(entry.getKey(), convertHeaderValueIfNecessary(entry.getValue())); } if (retryCount > 0) { writableHeaders.put(MessageProperties.RETRY_COUNT, retryCount); @@ -207,8 +208,7 @@ private Map convertHeadersIfNecessary(MessageProperties source) * @param valueArg the value. * @return the converted value. */ - @Nullable // NOSONAR complexity - private Object convertHeaderValueIfNecessary(@Nullable Object valueArg) { + private @Nullable Object convertHeaderValueIfNecessary(@Nullable Object valueArg) { Object value = valueArg; boolean valid = (value instanceof String) || (value instanceof byte[]) // NOSONAR boolean complexity || (value instanceof Boolean) || (value instanceof Class) @@ -220,14 +220,14 @@ private Object convertHeaderValueIfNecessary(@Nullable Object valueArg) { value = value.toString(); } else if (value instanceof Object[] array) { - Object[] writableArray = new Object[array.length]; + @Nullable Object[] writableArray = new Object[array.length]; for (int i = 0; i < writableArray.length; i++) { writableArray[i] = convertHeaderValueIfNecessary(array[i]); } value = writableArray; } else if (value instanceof List values) { - List writableList = new ArrayList<>(values.size()); + List<@Nullable Object> writableList = new ArrayList<>(values.size()); for (Object listValue : values) { writableList.add(convertHeaderValueIfNecessary(listValue)); } @@ -235,10 +235,10 @@ else if (value instanceof List values) { } else if (value instanceof Map) { @SuppressWarnings("unchecked") - Map originalMap = (Map) value; - Map writableMap = new HashMap<>(originalMap.size()); + Map originalMap = (Map) value; + Map writableMap = new HashMap<>(originalMap.size()); for (Map.Entry entry : originalMap.entrySet()) { - writableMap.put(entry.getKey(), this.convertHeaderValueIfNecessary(entry.getValue())); + writableMap.put(entry.getKey(), convertHeaderValueIfNecessary(entry.getValue())); } value = writableMap; } @@ -292,7 +292,7 @@ private Object convertLongStringIfNecessary(Object valueArg, String charset) { if (valueArg instanceof Map originalMap) { Map convertedMap = new HashMap<>(); originalMap.forEach( - (key, value) -> convertedMap.put((String) key, this.convertLongStringIfNecessary(value, charset))); + (key, value) -> convertedMap.put((String) key, this.convertLongStringIfNecessary(value, charset))); return convertedMap; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ListenerContainerAware.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ListenerContainerAware.java index bfee888a4e..a3711aed7f 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ListenerContainerAware.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ListenerContainerAware.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * Copyright 2015-2025 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. @@ -18,6 +18,8 @@ import java.util.Collection; +import org.jspecify.annotations.Nullable; + /** * {@link org.springframework.amqp.core.MessageListener}s that also implement this * interface can have configuration verified during initialization. @@ -34,6 +36,7 @@ public interface ListenerContainerAware { * * @return the queue names. */ + @Nullable Collection expectedQueueNames(); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ListenerExecutionFailedException.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ListenerExecutionFailedException.java index 40bd576a3a..a74c07d171 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ListenerExecutionFailedException.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ListenerExecutionFailedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -22,6 +22,8 @@ import java.util.Collections; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.AmqpException; import org.springframework.amqp.core.Message; @@ -31,6 +33,7 @@ * * @author Juergen Hoeller * @author Gary Russell + * @author Artem Bilan * * @see org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter */ @@ -50,8 +53,8 @@ public ListenerExecutionFailedException(String msg, Throwable cause, Message... this.failedMessages.addAll(Arrays.asList(failedMessage)); } - public Message getFailedMessage() { - return this.failedMessages.get(0); + public @Nullable Message getFailedMessage() { + return this.failedMessages.isEmpty() ? null : this.failedMessages.get(0); } public Collection getFailedMessages() { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/MessagePropertiesConverter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/MessagePropertiesConverter.java index 4f18deedb5..aa26e13046 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/MessagePropertiesConverter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/MessagePropertiesConverter.java @@ -18,9 +18,9 @@ import com.rabbitmq.client.AMQP.BasicProperties; import com.rabbitmq.client.Envelope; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.MessageProperties; -import org.springframework.lang.Nullable; /** * Strategy interface for converting between Spring AMQP {@link MessageProperties} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/RabbitExceptionTranslator.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/RabbitExceptionTranslator.java index c4b3825220..0ff98fed13 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/RabbitExceptionTranslator.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/RabbitExceptionTranslator.java @@ -24,6 +24,7 @@ import com.rabbitmq.client.ConsumerCancelledException; import com.rabbitmq.client.PossibleAuthenticationFailureException; import com.rabbitmq.client.ShutdownSignalException; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpAuthenticationException; import org.springframework.amqp.AmqpConnectException; @@ -32,7 +33,6 @@ import org.springframework.amqp.AmqpTimeoutException; import org.springframework.amqp.AmqpUnsupportedEncodingException; import org.springframework.amqp.UncategorizedAmqpException; -import org.springframework.util.Assert; /** * Translates Rabbit Exceptions to the {@link AmqpException} class @@ -49,8 +49,7 @@ public final class RabbitExceptionTranslator { private RabbitExceptionTranslator() { } - public static RuntimeException convertRabbitAccessException(Throwable ex) { - Assert.notNull(ex, "Exception must not be null"); + public static RuntimeException convertRabbitAccessException(@Nullable Throwable ex) { if (ex instanceof AmqpException amqpException) { return amqpException; } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ValueExpression.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ValueExpression.java index 6b228a2c57..a4dbda0dd0 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ValueExpression.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/ValueExpression.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2025 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. @@ -16,124 +16,133 @@ package org.springframework.amqp.rabbit.support; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; import org.springframework.expression.Expression; import org.springframework.expression.TypedValue; -import org.springframework.util.Assert; +import org.springframework.expression.common.ExpressionUtils; /** - * A very simple hardcoded implementation of the {@link org.springframework.expression.Expression} + * A very simple hardcoded implementation of the {@link Expression} * interface that represents an immutable value. * It is used as value holder in the context of expression evaluation. * * @param - The expected value type. * * @author Artem Bilan + * * @since 1.4 */ public class ValueExpression implements Expression { /** Fixed value of this expression. */ - private final V value; + private final @Nullable V value; - private final Class aClass; + private final @Nullable Class aClass; private final TypedValue typedResultValue; - private final TypeDescriptor typeDescriptor; + private final @Nullable TypeDescriptor typeDescriptor; @SuppressWarnings("unchecked") - public ValueExpression(V value) { - Assert.notNull(value, "'value' must not be null"); + public ValueExpression(@Nullable V value) { this.value = value; - this.aClass = (Class) this.value.getClass(); + this.aClass = (Class) (this.value != null ? this.value.getClass() : null); this.typedResultValue = new TypedValue(this.value); this.typeDescriptor = this.typedResultValue.getTypeDescriptor(); } @Override - public V getValue() throws EvaluationException { + public @Nullable V getValue() throws EvaluationException { return this.value; } @Override - public V getValue(Object rootObject) throws EvaluationException { + public @Nullable V getValue(@Nullable Object rootObject) throws EvaluationException { return this.value; } @Override - public V getValue(EvaluationContext context) throws EvaluationException { + public @Nullable V getValue(EvaluationContext context) throws EvaluationException { return this.value; } @Override - public V getValue(EvaluationContext context, Object rootObject) throws EvaluationException { + public @Nullable V getValue(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException { return this.value; } @Override - public T getValue(Object rootObject, Class desiredResultType) throws EvaluationException { + public @Nullable T getValue(@Nullable Object rootObject, @Nullable Class desiredResultType) + throws EvaluationException { + return getValue(desiredResultType); } @Override - public T getValue(Class desiredResultType) throws EvaluationException { - return org.springframework.expression.common.ExpressionUtils - .convertTypedValue(null, this.typedResultValue, desiredResultType); + public @Nullable T getValue(@Nullable Class desiredResultType) throws EvaluationException { + return ExpressionUtils.convertTypedValue(null, this.typedResultValue, desiredResultType); } @Override - public T getValue(EvaluationContext context, Object rootObject, Class desiredResultType) + public @Nullable T getValue(EvaluationContext context, @Nullable Object rootObject, + @Nullable Class desiredResultType) throws EvaluationException { + return getValue(context, desiredResultType); } @Override - public T getValue(EvaluationContext context, Class desiredResultType) throws EvaluationException { - return org.springframework.expression.common.ExpressionUtils - .convertTypedValue(context, this.typedResultValue, desiredResultType); + public @Nullable T getValue(EvaluationContext context, @Nullable Class desiredResultType) + throws EvaluationException { + + return ExpressionUtils.convertTypedValue(context, this.typedResultValue, desiredResultType); } @Override - public Class getValueType() throws EvaluationException { + public @Nullable Class getValueType() throws EvaluationException { return this.aClass; } @Override - public Class getValueType(Object rootObject) throws EvaluationException { + public @Nullable Class getValueType(@Nullable Object rootObject) throws EvaluationException { return this.aClass; } @Override - public Class getValueType(EvaluationContext context) throws EvaluationException { + public @Nullable Class getValueType(EvaluationContext context) throws EvaluationException { return this.aClass; } @Override - public Class getValueType(EvaluationContext context, Object rootObject) throws EvaluationException { + public @Nullable Class getValueType(EvaluationContext context, @Nullable Object rootObject) + throws EvaluationException { + return this.aClass; } @Override - public TypeDescriptor getValueTypeDescriptor() throws EvaluationException { + public @Nullable TypeDescriptor getValueTypeDescriptor() throws EvaluationException { return this.typeDescriptor; } @Override - public TypeDescriptor getValueTypeDescriptor(Object rootObject) throws EvaluationException { + public @Nullable TypeDescriptor getValueTypeDescriptor(@Nullable Object rootObject) throws EvaluationException { return this.typeDescriptor; } @Override - public TypeDescriptor getValueTypeDescriptor(EvaluationContext context) throws EvaluationException { + public @Nullable TypeDescriptor getValueTypeDescriptor(EvaluationContext context) throws EvaluationException { return this.typeDescriptor; } @Override - public TypeDescriptor getValueTypeDescriptor(EvaluationContext context, Object rootObject) + public @Nullable TypeDescriptor getValueTypeDescriptor(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException { + return this.typeDescriptor; } @@ -143,33 +152,35 @@ public boolean isWritable(EvaluationContext context) throws EvaluationException } @Override - public boolean isWritable(EvaluationContext context, Object rootObject) throws EvaluationException { + public boolean isWritable(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException { return false; } @Override - public boolean isWritable(Object rootObject) throws EvaluationException { + public boolean isWritable(@Nullable Object rootObject) throws EvaluationException { return false; } @Override - public void setValue(EvaluationContext context, Object value) throws EvaluationException { + public void setValue(EvaluationContext context, @Nullable Object value) throws EvaluationException { setValue(context, null, value); } @Override - public void setValue(Object rootObject, Object value) throws EvaluationException { + public void setValue(@Nullable Object rootObject, @Nullable Object value) throws EvaluationException { setValue(null, rootObject, value); } @Override - public void setValue(EvaluationContext context, Object rootObject, Object value) throws EvaluationException { - throw new EvaluationException(this.value.toString(), "Cannot call setValue() on a ValueExpression"); + public void setValue(@Nullable EvaluationContext context, @Nullable Object rootObject, @Nullable Object value) + throws EvaluationException { + + throw new EvaluationException(getExpressionString(), "Cannot call setValue() on a ValueExpression"); } @Override public String getExpressionString() { - return this.value.toString(); + return this.value != null ? this.value.toString() : "null"; } } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java index ee29b9b848..52abc4f4d5 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java @@ -17,6 +17,7 @@ package org.springframework.amqp.rabbit.support.micrometer; import io.micrometer.observation.transport.ReceiverContext; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.Message; @@ -24,6 +25,8 @@ * {@link ReceiverContext} for {@link Message}s. * * @author Gary Russell + * @author Artem Bilan + * * @since 3.0 * */ @@ -33,6 +36,7 @@ public class RabbitMessageReceiverContext extends ReceiverContext { private final Message message; + @SuppressWarnings("this-escape") public RabbitMessageReceiverContext(Message message, String listenerId) { super((carrier, key) -> carrier.getMessageProperties().getHeader(key)); setCarrier(message); @@ -49,7 +53,7 @@ public String getListenerId() { * Return the source (queue) for this message. * @return the source. */ - public String getSource() { + public @Nullable String getSource() { return this.message.getMessageProperties().getConsumerQueue(); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageSenderContext.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageSenderContext.java index c6521abbb5..dfd1d6d216 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageSenderContext.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageSenderContext.java @@ -38,18 +38,6 @@ public class RabbitMessageSenderContext extends SenderContext { private final String routingKey; - @Deprecated(since = "3.2") - public RabbitMessageSenderContext(Message message, String beanName, String destination) { - super((carrier, key, value) -> message.getMessageProperties().setHeader(key, value)); - setCarrier(message); - this.beanName = beanName; - this.exchange = null; - this.routingKey = null; - this.destination = destination; - setRemoteServiceName("RabbitMQ"); - } - - /** * Create an instance {@code RabbitMessageSenderContext}. * @param message a message to send @@ -58,6 +46,7 @@ public RabbitMessageSenderContext(Message message, String beanName, String desti * @param routingKey the routing key * @since 3.2 */ + @SuppressWarnings("this-escape") public RabbitMessageSenderContext(Message message, String beanName, String exchange, String routingKey) { super((carrier, key, value) -> message.getMessageProperties().setHeader(key, value)); setCarrier(message); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/package-info.java index 8131427dac..07a19f163e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/package-info.java @@ -1,6 +1,5 @@ /** * Provides classes for Micrometer support. */ -@org.springframework.lang.NonNullApi -@org.springframework.lang.NonNullFields +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.support.micrometer; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/package-info.java index 59f6f0ca85..0a94360493 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/package-info.java @@ -1,5 +1,5 @@ /** * Provides support classes for Spring Rabbit. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.support; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/transaction/RabbitTransactionManager.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/transaction/RabbitTransactionManager.java index 5ff76df564..6da04d3efd 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/transaction/RabbitTransactionManager.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/transaction/RabbitTransactionManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2025 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. @@ -16,13 +16,19 @@ package org.springframework.amqp.rabbit.transaction; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.AmqpException; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.Connection; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactoryUtils; import org.springframework.amqp.rabbit.connection.RabbitResourceHolder; +import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.InitializingBean; import org.springframework.transaction.CannotCreateTransactionException; import org.springframework.transaction.InvalidIsolationLevelException; +import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.support.AbstractPlatformTransactionManager; import org.springframework.transaction.support.DefaultTransactionStatus; @@ -32,7 +38,7 @@ import org.springframework.util.Assert; /** - * {@link org.springframework.transaction.PlatformTransactionManager} implementation for a single Rabbit + * {@link PlatformTransactionManager} implementation for a single Rabbit * {@link ConnectionFactory}. Binds a Rabbit Channel from the specified ConnectionFactory to the thread, potentially * allowing for one thread-bound channel per ConnectionFactory. * @@ -44,13 +50,13 @@ *

* Application code is required to retrieve the transactional Rabbit resources via * {@link ConnectionFactoryUtils#getTransactionalResourceHolder(ConnectionFactory, boolean)} instead of a standard - * {@link org.springframework.amqp.rabbit.connection.Connection#createChannel(boolean)} call with subsequent + * {@link Connection#createChannel(boolean)} call with subsequent * Channel creation. Spring's - * {@link org.springframework.amqp.rabbit.core.RabbitTemplate} will + * {@link RabbitTemplate} will * autodetect a thread-bound Channel and automatically participate in it. * *

- * The use of {@link org.springframework.amqp.rabbit.connection.CachingConnectionFactory} + * The use of {@link CachingConnectionFactory} * as a target for this transaction manager is strongly recommended. * CachingConnectionFactory uses a single Rabbit Connection for all Rabbit access in order to avoid the overhead of * repeated Connection creation, as well as maintaining a cache of Channels. Each transaction will then share the same @@ -62,12 +68,13 @@ * which has stronger needs for synchronization. * * @author Dave Syer + * @author Artem Bilan */ @SuppressWarnings("serial") public class RabbitTransactionManager extends AbstractPlatformTransactionManager implements ResourceTransactionManager, InitializingBean { - private ConnectionFactory connectionFactory; + private @Nullable ConnectionFactory connectionFactory; /** * Create a new RabbitTransactionManager for bean-style usage. @@ -81,6 +88,7 @@ public class RabbitTransactionManager extends AbstractPlatformTransactionManager * @see #setConnectionFactory * @see #setTransactionSynchronization */ + @SuppressWarnings("this-escape") public RabbitTransactionManager() { setTransactionSynchronization(SYNCHRONIZATION_NEVER); } @@ -104,7 +112,7 @@ public void setConnectionFactory(ConnectionFactory connectionFactory) { /** * @return the connectionFactory */ - public ConnectionFactory getConnectionFactory() { + public @Nullable ConnectionFactory getConnectionFactory() { return this.connectionFactory; } @@ -118,14 +126,16 @@ public void afterPropertiesSet() { @Override public Object getResourceFactory() { - return getConnectionFactory(); + ConnectionFactory resourceFactory = getConnectionFactory(); + Assert.notNull(resourceFactory, "'connectionFactory' cannot be null"); + return resourceFactory; } @Override protected Object doGetTransaction() { RabbitTransactionObject txObject = new RabbitTransactionObject(); txObject.setResourceHolder((RabbitResourceHolder) TransactionSynchronizationManager - .getResource(getConnectionFactory())); + .getResource(getResourceFactory())); return txObject; } @@ -143,18 +153,20 @@ protected void doBegin(Object transaction, TransactionDefinition definition) { RabbitTransactionObject txObject = (RabbitTransactionObject) transaction; RabbitResourceHolder resourceHolder = null; try { - resourceHolder = ConnectionFactoryUtils.getTransactionalResourceHolder(getConnectionFactory(), true); + ConnectionFactory connectionFactoryToUse = getConnectionFactory(); + Assert.notNull(connectionFactoryToUse, "'connectionFactory' cannot be null"); + resourceHolder = ConnectionFactoryUtils.getTransactionalResourceHolder(connectionFactoryToUse, true); if (logger.isDebugEnabled()) { logger.debug("Created AMQP transaction on channel [" + resourceHolder.getChannel() + "]"); } // resourceHolder.declareTransactional(); txObject.setResourceHolder(resourceHolder); - txObject.getResourceHolder().setSynchronizedWithTransaction(true); + resourceHolder.setSynchronizedWithTransaction(true); int timeout = determineTimeout(definition); if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) { - txObject.getResourceHolder().setTimeoutInSeconds(timeout); + resourceHolder.setTimeoutInSeconds(timeout); } - TransactionSynchronizationManager.bindResource(getConnectionFactory(), txObject.getResourceHolder()); + TransactionSynchronizationManager.bindResource(connectionFactoryToUse, resourceHolder); } catch (AmqpException ex) { if (resourceHolder != null) { @@ -168,41 +180,51 @@ protected void doBegin(Object transaction, TransactionDefinition definition) { protected Object doSuspend(Object transaction) { RabbitTransactionObject txObject = (RabbitTransactionObject) transaction; txObject.setResourceHolder(null); - return TransactionSynchronizationManager.unbindResource(getConnectionFactory()); + return TransactionSynchronizationManager.unbindResource(getResourceFactory()); } @Override - protected void doResume(Object transaction, Object suspendedResources) { + protected void doResume(@Nullable Object transaction, Object suspendedResources) { RabbitResourceHolder conHolder = (RabbitResourceHolder) suspendedResources; - TransactionSynchronizationManager.bindResource(getConnectionFactory(), conHolder); + TransactionSynchronizationManager.bindResource(getResourceFactory(), conHolder); } @Override protected void doCommit(DefaultTransactionStatus status) { RabbitTransactionObject txObject = (RabbitTransactionObject) status.getTransaction(); RabbitResourceHolder resourceHolder = txObject.getResourceHolder(); - resourceHolder.commitAll(); + if (resourceHolder != null) { + resourceHolder.commitAll(); + } } @Override protected void doRollback(DefaultTransactionStatus status) { RabbitTransactionObject txObject = (RabbitTransactionObject) status.getTransaction(); RabbitResourceHolder resourceHolder = txObject.getResourceHolder(); - resourceHolder.rollbackAll(); + if (resourceHolder != null) { + resourceHolder.rollbackAll(); + } } @Override protected void doSetRollbackOnly(DefaultTransactionStatus status) { RabbitTransactionObject txObject = (RabbitTransactionObject) status.getTransaction(); - txObject.getResourceHolder().setRollbackOnly(); + RabbitResourceHolder resourceHolder = txObject.getResourceHolder(); + if (resourceHolder != null) { + resourceHolder.setRollbackOnly(); + } } @Override protected void doCleanupAfterCompletion(Object transaction) { RabbitTransactionObject txObject = (RabbitTransactionObject) transaction; - TransactionSynchronizationManager.unbindResource(getConnectionFactory()); - txObject.getResourceHolder().closeAll(); - txObject.getResourceHolder().clear(); + TransactionSynchronizationManager.unbindResource(getResourceFactory()); + RabbitResourceHolder resourceHolder = txObject.getResourceHolder(); + if (resourceHolder != null) { + resourceHolder.closeAll(); + resourceHolder.clear(); + } } /** @@ -212,27 +234,29 @@ protected void doCleanupAfterCompletion(Object transaction) { */ private static class RabbitTransactionObject implements SmartTransactionObject { - private RabbitResourceHolder resourceHolder; + private @Nullable RabbitResourceHolder resourceHolder; RabbitTransactionObject() { } - public void setResourceHolder(RabbitResourceHolder resourceHolder) { + public void setResourceHolder(@Nullable RabbitResourceHolder resourceHolder) { this.resourceHolder = resourceHolder; } - public RabbitResourceHolder getResourceHolder() { + public @Nullable RabbitResourceHolder getResourceHolder() { return this.resourceHolder; } @Override public boolean isRollbackOnly() { - return this.resourceHolder.isRollbackOnly(); + return this.resourceHolder != null && this.resourceHolder.isRollbackOnly(); } @Override public void flush() { // no-op } + } + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/transaction/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/transaction/package-info.java index 28c0c93c70..167073bec6 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/transaction/package-info.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/transaction/package-info.java @@ -1,4 +1,5 @@ /** * Provides classes supporting transactions in Spring Rabbit. */ +@org.jspecify.annotations.NullMarked package org.springframework.amqp.rabbit.transaction; diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java index 5478f9bf5a..f407f6b03b 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java @@ -119,7 +119,6 @@ import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.task.TaskExecutor; import org.springframework.data.web.JsonPath; -import org.springframework.lang.NonNull; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.converter.GenericMessageConverter; import org.springframework.messaging.handler.annotation.Header; @@ -2073,7 +2072,7 @@ static class MultiListenerBean { @RabbitHandler @SendTo("${foo.bar:#{sendToRepliesBean}}") - public String bar(@NonNull Bar bar) { + public String bar(Bar bar) { if (bar.field.equals("crash")) { throw new RuntimeException("Test reply from error handler"); } @@ -2088,7 +2087,7 @@ public String baz(Baz baz, Message message) { } @RabbitHandler - public String qux(@Header("amqp_receivedRoutingKey") String rk, @NonNull @Payload Qux qux) { + public String qux(@Header("amqp_receivedRoutingKey") String rk, @Payload Qux qux) { return "QUX: " + qux.field + ": " + rk; } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/MessageListenerTestContainer.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/MessageListenerTestContainer.java index 16c6e2960d..e014ddfaa7 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/MessageListenerTestContainer.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/MessageListenerTestContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 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. @@ -16,12 +16,13 @@ package org.springframework.amqp.rabbit.config; +import org.jspecify.annotations.Nullable; + import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.rabbit.listener.MessageListenerContainer; import org.springframework.amqp.rabbit.listener.RabbitListenerEndpoint; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; /** * @author Stephane Nicoll @@ -57,8 +58,7 @@ public void setAutoStartup(boolean autoStart) { } @Override - @Nullable - public Object getMessageListener() { + public @Nullable Object getMessageListener() { return null; } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java index 7746bf955d..e4d4a223d5 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/CachingConnectionFactoryTests.java @@ -558,10 +558,10 @@ private void testCheckoutsWithRefreshedConnectionGuts(CacheMode mode) throws Exc } @Test - public void testCheckoutLimitWithRelease() throws IOException, Exception { - com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class); - com.rabbitmq.client.Connection mockConnection = mock(com.rabbitmq.client.Connection.class); - Channel mockChannel1 = mock(Channel.class); + public void testCheckoutLimitWithRelease() throws Exception { + com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(); + com.rabbitmq.client.Connection mockConnection = mock(); + Channel mockChannel1 = mock(); given(mockConnectionFactory.newConnection(any(ExecutorService.class), anyString())).willReturn(mockConnection); given(mockConnection.createChannel()).willReturn(mockChannel1); @@ -610,19 +610,19 @@ public void testCheckoutLimitWithRelease() throws IOException, Exception { } @Test - public void testCheckoutLimitWithPublisherConfirmsLogical() throws IOException, Exception { + public void testCheckoutLimitWithPublisherConfirmsLogical() throws Exception { testCheckoutLimitWithPublisherConfirms(false); } @Test - public void testCheckoutLimitWithPublisherConfirmsPhysical() throws IOException, Exception { + public void testCheckoutLimitWithPublisherConfirmsPhysical() throws Exception { testCheckoutLimitWithPublisherConfirms(true); } - private void testCheckoutLimitWithPublisherConfirms(boolean physicalClose) throws IOException, Exception { - com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class); - com.rabbitmq.client.Connection mockConnection = mock(com.rabbitmq.client.Connection.class); - Channel mockChannel = mock(Channel.class); + private void testCheckoutLimitWithPublisherConfirms(boolean physicalClose) throws Exception { + com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(); + com.rabbitmq.client.Connection mockConnection = mock(); + Channel mockChannel = mock(); given(mockConnectionFactory.newConnection(any(ExecutorService.class), anyString())).willReturn(mockConnection); given(mockConnection.createChannel()).willReturn(mockChannel); @@ -654,7 +654,7 @@ private void testCheckoutLimitWithPublisherConfirms(boolean physicalClose) throw RabbitTemplate rabbitTemplate = new RabbitTemplate(ccf); if (physicalClose) { Channel channel1 = con.createChannel(false); - RabbitUtils.setPhysicalCloseRequired(channel1, physicalClose); + RabbitUtils.setPhysicalCloseRequired(channel1, true); channel1.close(); } else { @@ -693,7 +693,7 @@ private void testCheckoutLimitWithPublisherConfirms(boolean physicalClose) throw } @Test - public void testCheckoutLimitWithPublisherConfirmsLogicalAlreadyCloses() throws IOException, Exception { + public void testCheckoutLimitWithPublisherConfirmsLogicalAlreadyCloses() throws Exception { com.rabbitmq.client.ConnectionFactory mockConnectionFactory = mock(com.rabbitmq.client.ConnectionFactory.class); com.rabbitmq.client.Connection mockConnection = mock(com.rabbitmq.client.Connection.class); Channel mockChannel = mock(Channel.class); @@ -1609,7 +1609,7 @@ private void testConsumerChannelPhysicallyClosedWhenNotIsOpenGuts(boolean confir Channel channel = con.createChannel(false); RabbitUtils.setPhysicalCloseRequired(channel, true); - given(mockChannel.isOpen()).willReturn(false); + given(mockChannel.isOpen()).willReturn(true); final CountDownLatch physicalCloseLatch = new CountDownLatch(1); willAnswer(i -> { physicalCloseLatch.countDown(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/NodeLocatorTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/NodeLocatorTests.java index 9657dd1d18..448d7a582c 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/NodeLocatorTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/NodeLocatorTests.java @@ -19,11 +19,10 @@ import java.net.URISyntaxException; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.lang.Nullable; - import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -31,6 +30,8 @@ /** * @author Gary Russell + * @author Artem Bilan + * * @since 3.0 * */ @@ -44,11 +45,10 @@ void missingNode() throws URISyntaxException { @Override public Object createClient(String userName, String password) { - return null; + return new Object(); } @Override - @Nullable public Map restCall(Object client, String baseUri, String vhost, String queue) { if (baseUri.contains("foo")) { return Map.of("node", "c@d"); @@ -59,10 +59,8 @@ public Map restCall(Object client, String baseUri, String vhost, } }); - ConnectionFactory factory = nodeLocator.locate(new String[] { "http://foo", "http://bar" }, - Map.of("a@b", "baz"), null, "q", null, null, (q, n, u) -> { - return null; - }); + ConnectionFactory factory = nodeLocator.locate(new String[] {"http://foo", "http://bar"}, + Map.of("a@b", "baz"), "/", "q", "guest", "guest", (q, n, u) -> null); verify(nodeLocator, times(2)).restCall(any(), any(), any(), any()); } @@ -74,12 +72,13 @@ void nullInfo() throws URISyntaxException { @Override public Object createClient(String userName, String password) { - return null; + return new Object(); } @Override @Nullable public Map restCall(Object client, String baseUri, String vhost, String queue) { + if (baseUri.contains("foo")) { return null; } @@ -89,10 +88,8 @@ public Map restCall(Object client, String baseUri, String vhost, } }); - ConnectionFactory factory = nodeLocator.locate(new String[] { "http://foo", "http://bar" }, - Map.of("a@b", "baz"), null, "q", null, null, (q, n, u) -> { - return null; - }); + ConnectionFactory factory = nodeLocator.locate(new String[] {"http://foo", "http://bar"}, + Map.of("a@b", "baz"), "/", "q", "guest", "guest", (q, n, u) -> null); verify(nodeLocator, times(2)).restCall(any(), any(), any(), any()); } @@ -104,7 +101,7 @@ void notFound() throws URISyntaxException { @Override public Object createClient(String userName, String password) { - return null; + return new Object(); } @Override @@ -114,10 +111,8 @@ public Map restCall(Object client, String baseUri, String vhost, } }); - ConnectionFactory factory = nodeLocator.locate(new String[] { "http://foo", "http://bar" }, - Map.of("a@b", "baz"), null, "q", null, null, (q, n, u) -> { - return null; - }); + ConnectionFactory factory = nodeLocator.locate(new String[] {"http://foo", "http://bar"}, + Map.of("a@b", "baz"), "/", "q", "guest", "guest", (q, n, u) -> null); verify(nodeLocator, times(2)).restCall(any(), any(), any(), any()); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactoryTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactoryTests.java index b1f747010a..d8a799fb51 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactoryTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/PooledChannelConnectionFactoryTests.java @@ -25,6 +25,7 @@ import com.rabbitmq.client.Channel; import com.rabbitmq.client.ConnectionFactory; import org.apache.commons.pool2.impl.GenericObjectPool; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Queue; @@ -273,6 +274,7 @@ public static class Config { boolean closed; + @Nullable Connection connection; boolean channelCreated; @@ -284,27 +286,20 @@ PooledChannelConnectionFactory pccf() { pccf.addConnectionListener(new ConnectionListener() { @Override - public void onCreate(Connection connection) { + public void onCreate(@Nullable Connection connection) { Config.this.connection = connection; Config.this.created = true; } @Override public void onClose(Connection connection) { - if (Config.this.connection.equals(connection)) { + if (connection.equals(Config.this.connection)) { Config.this.closed = true; } } }); - pccf.addChannelListener(new ChannelListener() { - - @Override - public void onCreate(Channel channel, boolean transactional) { - Config.this.channelCreated = true; - } - - }); + pccf.addChannelListener((channel, transactional) -> Config.this.channelCreated = true); return pccf; } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SingleConnectionFactory.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SingleConnectionFactory.java index 085ba77ecc..62c0e41a58 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SingleConnectionFactory.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/connection/SingleConnectionFactory.java @@ -21,6 +21,7 @@ import com.rabbitmq.client.BlockedListener; import com.rabbitmq.client.Channel; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpException; import org.springframework.util.StringUtils; @@ -39,7 +40,7 @@ public class SingleConnectionFactory extends AbstractConnectionFactory { /** Proxy Connection */ - private SharedConnectionProxy connection; + private @Nullable SharedConnectionProxy connection; /** Synchronization monitor for the shared Connection */ private final Object connectionMonitor = new Object(); @@ -64,7 +65,7 @@ public SingleConnectionFactory(int port) { * Create a new SingleConnectionFactory given a host name. * @param hostname the host name to connect to */ - public SingleConnectionFactory(String hostname) { + public SingleConnectionFactory(@Nullable String hostname) { this(hostname, com.rabbitmq.client.ConnectionFactory.DEFAULT_AMQP_PORT); } @@ -73,7 +74,8 @@ public SingleConnectionFactory(String hostname) { * @param hostname the host name to connect to * @param port the port number to connect to */ - public SingleConnectionFactory(String hostname, int port) { + @SuppressWarnings("this-escape") + public SingleConnectionFactory(@Nullable String hostname, int port) { super(new com.rabbitmq.client.ConnectionFactory()); if (!StringUtils.hasText(hostname)) { hostname = getDefaultHostName(); @@ -86,6 +88,7 @@ public SingleConnectionFactory(String hostname, int port) { * Create a new SingleConnectionFactory given a {@link URI}. * @param uri the amqp uri configuring the connection */ + @SuppressWarnings("this-escape") public SingleConnectionFactory(URI uri) { super(new com.rabbitmq.client.ConnectionFactory()); setUri(uri); @@ -152,8 +155,7 @@ public final void destroy() { * @return the new Connection */ protected Connection doCreateConnection() { - Connection connection = createBareConnection(); - return connection; + return createBareConnection(); } @Override @@ -205,16 +207,13 @@ public void close() { } public void destroy() { - if (this.target != null) { - getConnectionListener().onClose(target); - RabbitUtils.closeConnection(this.target); - } - this.target = null; + getConnectionListener().onClose(target); + RabbitUtils.closeConnection(this.target); } @Override public boolean isOpen() { - return target != null && target.isOpen(); + return target.isOpen(); } @Override @@ -224,16 +223,17 @@ public Connection getTargetConnection() { @Override public int getLocalPort() { - Connection target = this.target; - if (target != null) { - return target.getLocalPort(); - } - return 0; + return this.target.getLocalPort(); + } + + @Override + public com.rabbitmq.client.Connection getDelegate() { + return this.target.getDelegate(); } @Override public int hashCode() { - return 31 + ((target == null) ? 0 : target.hashCode()); + return 31 + target.hashCode(); } @Override @@ -248,15 +248,7 @@ public boolean equals(Object obj) { return false; } SharedConnectionProxy other = (SharedConnectionProxy) obj; - if (target == null) { - if (other.target != null) { - return false; - } - } - else if (!target.equals(other.target)) { - return false; - } - return true; + return target.equals(other.target); } @Override diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplateTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplateTests.java index 2fd8d9db83..08dc224c82 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplateTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/BatchingRabbitTemplateTests.java @@ -62,7 +62,6 @@ import org.springframework.amqp.support.postprocessor.ZipPostProcessor; import org.springframework.amqp.utils.test.TestUtils; import org.springframework.beans.DirectFieldAccessor; -import org.springframework.lang.Nullable; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.util.ReflectionUtils; import org.springframework.util.StopWatch; @@ -343,7 +342,8 @@ public void testDebatchByContainerPerformance() throws Exception { public void testDebatchByContainerBadMessageRejected() throws Exception { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(this.connectionFactory); container.setQueueNames(ROUTE); - container.setMessageListener(message -> { }); + container.setMessageListener(message -> { + }); container.setReceiveTimeout(10); ConditionalRejectingErrorHandler errorHandler = new ConditionalRejectingErrorHandler(); container.setErrorHandler(errorHandler); @@ -623,8 +623,7 @@ public void testSimpleBatchDeflaterWithEncoding() throws Exception { assertThat(new String(message.getBody())).isEqualTo("\u0000\u0000\u0000\u0003foo\u0000\u0000\u0000\u0003bar"); } - @Nullable - private Message receive(BatchingRabbitTemplate template) throws InterruptedException { + private Message receive(BatchingRabbitTemplate template) { return await().with().pollInterval(Duration.ofMillis(50)) .until(() -> template.receive(ROUTE), msg -> msg != null); } @@ -676,10 +675,13 @@ private int getStreamLevel(Object stream) throws Exception { } private static final class HeaderPostProcessor implements MessagePostProcessor { + @Override public Message postProcessMessage(Message message) throws AmqpException { message.getMessageProperties().getHeaders().put("someHeader", "someValue"); return message; } + } + } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/MessagingTemplateConfirmsTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/MessagingTemplateConfirmsTests.java index d537784d34..56e69c0159 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/MessagingTemplateConfirmsTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/MessagingTemplateConfirmsTests.java @@ -49,7 +49,7 @@ void confirmHeader() throws Exception { CorrelationData data = new CorrelationData(); rmt.send("messaging.confirms", new GenericMessage<>("foo", Collections.singletonMap(AmqpHeaders.PUBLISH_CONFIRM_CORRELATION, data))); - assertThat(data.getFuture().get(10, TimeUnit.SECONDS).isAck()).isTrue(); + assertThat(data.getFuture().get(10, TimeUnit.SECONDS).ack()).isTrue(); ccf.destroy(); } @@ -65,7 +65,7 @@ void confirmHeaderUnroutable() throws Exception { CorrelationData data = new CorrelationData("foo"); rmt.send("messaging.confirms.unroutable", new GenericMessage<>("foo", Collections.singletonMap(AmqpHeaders.PUBLISH_CONFIRM_CORRELATION, data))); - assertThat(data.getFuture().get(10, TimeUnit.SECONDS).isAck()).isTrue(); + assertThat(data.getFuture().get(10, TimeUnit.SECONDS).ack()).isTrue(); assertThat(data.getReturned()).isNotNull(); ccf.destroy(); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplateTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplateTests.java index d14e75815c..167918ff8c 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplateTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitMessagingTemplateTests.java @@ -59,6 +59,7 @@ /** * @author Stephane Nicoll * @author Gary Russell + * @author Artem Bilan */ public class RabbitMessagingTemplateTests { @@ -75,6 +76,7 @@ public class RabbitMessagingTemplateTests { @BeforeEach public void setup() { this.openMocks = MockitoAnnotations.openMocks(this); + given(this.rabbitTemplate.getMessageConverter()).willReturn(new SimpleMessageConverter()); messagingTemplate = new RabbitMessagingTemplate(rabbitTemplate); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateIntegrationTests.java index 10abeb6c3c..5baa4dfd70 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateIntegrationTests.java @@ -17,6 +17,7 @@ package org.springframework.amqp.rabbit.core; import java.io.IOException; +import java.io.PrintStream; import java.lang.reflect.Field; import java.util.Arrays; import java.util.Collection; @@ -67,7 +68,6 @@ import org.springframework.amqp.core.Queue; import org.springframework.amqp.core.ReceiveAndReplyCallback; import org.springframework.amqp.core.ReceiveAndReplyMessageCallback; -import org.springframework.amqp.core.ReplyToAddressCallback; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory.ConfirmType; import org.springframework.amqp.rabbit.connection.ChannelListener; @@ -142,12 +142,12 @@ * @author Artem Bilan */ @SpringJUnitConfig -@RabbitAvailable({ RabbitTemplateIntegrationTests.ROUTE, RabbitTemplateIntegrationTests.REPLY_QUEUE_NAME, - RabbitTemplateIntegrationTests.NO_CORRELATION }) -@LogLevels(classes = { RabbitTemplate.class, DirectMessageListenerContainer.class, - DirectReplyToMessageListenerContainer.class, - RabbitAdmin.class, RabbitTemplateIntegrationTests.class, BrokerRunning.class, - ClosingRecoveryListener.class }, +@RabbitAvailable({RabbitTemplateIntegrationTests.ROUTE, RabbitTemplateIntegrationTests.REPLY_QUEUE_NAME, + RabbitTemplateIntegrationTests.NO_CORRELATION}) +@LogLevels(classes = {RabbitTemplate.class, DirectMessageListenerContainer.class, + DirectReplyToMessageListenerContainer.class, + RabbitAdmin.class, RabbitTemplateIntegrationTests.class, BrokerRunning.class, + ClosingRecoveryListener.class}, level = "DEBUG") @DirtiesContext public class RabbitTemplateIntegrationTests { @@ -393,7 +393,7 @@ public void testReceiveTimeoutRequeue() { // empty - race for consumeOk } assertThat(TestUtils.getPropertyValue(this.connectionFactory, "cachedChannelsNonTransactional", List.class) - ).hasSize(0); + ).hasSize(0); } @Test @@ -449,19 +449,19 @@ public void testSendToNonExistentAndThenReceive() throws Exception { @Test public void testSendAndReceiveWithPostProcessor() throws Exception { - final String[] strings = new String[] { "1", "2" }; + final String[] strings = new String[] {"1", "2"}; template.convertAndSend(ROUTE, (Object) "message", message -> { message.getMessageProperties().setContentType("text/other"); // message.getMessageProperties().setUserId("foo"); MessageProperties props = message.getMessageProperties(); props.getHeaders().put("strings", strings); - props.getHeaders().put("objects", new Object[] { new Foo(), new Foo() }); + props.getHeaders().put("objects", new Object[] {new Foo(), new Foo()}); props.getHeaders().put("bytes", "abc".getBytes()); return message; }); template.setAfterReceivePostProcessors(message -> { assertThat(message.getMessageProperties().getHeaders().get("strings")).isEqualTo(Arrays.asList(strings)); - assertThat(message.getMessageProperties().getHeaders().get("objects")).isEqualTo(Arrays.asList(new String[]{"FooAsAString", "FooAsAString"})); + assertThat(message.getMessageProperties().getHeaders().get("objects")).isEqualTo(Arrays.asList(new String[] {"FooAsAString", "FooAsAString"})); assertThat((byte[]) message.getMessageProperties().getHeaders().get("bytes")).isEqualTo("abc".getBytes()); return message; }); @@ -1183,9 +1183,9 @@ private void testReceiveAndReply(long timeout) throws Exception { @Override public void doInTransactionWithoutResult(TransactionStatus status) { template.receiveAndReply((ReceiveAndReplyMessageCallback) message -> message, - (ReplyToAddressCallback) (request, reply) -> { - throw new PlannedException(); - }); + (request, reply) -> { + throw new PlannedException(); + }); } }); fail("Expected PlannedException"); @@ -1635,7 +1635,7 @@ public void testReceiveNoAutoRecovery() throws Exception { catch (AmqpException e) { e.printStackTrace(); if (e.getCause() != null - && e.getCause().getClass().equals(InterruptedException.class)) { + && e.getCause().getClass().equals(InterruptedException.class)) { Thread.currentThread().interrupt(); return; } @@ -1646,11 +1646,12 @@ public void testReceiveNoAutoRecovery() throws Exception { } } }); - System .out .println("Wait for consumer; then bounce broker; then enter after it's back up"); + PrintStream sout = System.out; + sout.println("Wait for consumer; then bounce broker; then enter after it's back up"); System.in.read(); for (int i = 0; i < 20; i++) { Properties queueProperties = admin.getQueueProperties(ROUTE); - System .out .println(queueProperties); + sout.println(queueProperties); Thread.sleep(1000); } exec.shutdownNow(); @@ -1729,6 +1730,7 @@ private class PlannedException extends RuntimeException { PlannedException() { super("Planned"); } + } @SuppressWarnings("serial") diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration1Tests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration1Tests.java index 1c0fda2a71..c56bb6c571 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration1Tests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration1Tests.java @@ -41,6 +41,7 @@ import com.rabbitmq.client.ConnectionFactory; import org.apache.commons.logging.Log; import org.assertj.core.api.InstanceOfAssertFactories; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -72,6 +73,7 @@ import org.springframework.context.event.ContextClosedEvent; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; @@ -200,7 +202,9 @@ public Message postProcessMessage(Message message) throws AmqpException { } @Override - public Message postProcessMessage(Message message, Correlation correlation, String exch, String rk) { + public Message postProcessMessage(Message message, @Nullable Correlation correlation, + String exch, String rk) { + assertThat(exch).isEqualTo(""); assertThat(rk).isEqualTo(ROUTE); mppLatch.countDown(); @@ -217,7 +221,7 @@ public Message postProcessMessage(Message message, Correlation correlation, Stri } } catch (Throwable t) { - t.printStackTrace(); + ReflectionUtils.rethrowRuntimeException(t); } }); } @@ -878,14 +882,14 @@ public void testWithFuture() throws Exception { admin.declareQueue(queue); CorrelationData cd1 = new CorrelationData(); this.templateWithConfirmsEnabled.convertAndSend("", queue.getName(), "foo", cd1); - assertThat(cd1.getFuture().get(10, TimeUnit.SECONDS).isAck()).isTrue(); + assertThat(cd1.getFuture().get(10, TimeUnit.SECONDS).ack()).isTrue(); CorrelationData cd2 = new CorrelationData(); this.templateWithConfirmsEnabled.convertAndSend("", queue.getName(), "bar", cd2); - assertThat(cd2.getFuture().get(10, TimeUnit.SECONDS).isAck()).isFalse(); + assertThat(cd2.getFuture().get(10, TimeUnit.SECONDS).ack()).isFalse(); CorrelationData cd3 = new CorrelationData(); this.templateWithConfirmsEnabled.convertAndSend("NO_EXCHANGE_HERE", queue.getName(), "foo", cd3); - assertThat(cd3.getFuture().get(10, TimeUnit.SECONDS).isAck()).isFalse(); - assertThat(cd3.getFuture().get().getReason()).contains("NOT_FOUND"); + assertThat(cd3.getFuture().get(10, TimeUnit.SECONDS).ack()).isFalse(); + assertThat(cd3.getFuture().get().reason()).contains("NOT_FOUND"); CorrelationData cd4 = new CorrelationData("42"); AtomicBoolean resent = new AtomicBoolean(); AtomicReference callbackThreadName = new AtomicReference<>(); @@ -897,7 +901,7 @@ public void testWithFuture() throws Exception { callbackLatch.countDown(); }); this.templateWithConfirmsAndReturnsEnabled.convertAndSend("", "NO_QUEUE_HERE", "foo", cd4); - assertThat(cd4.getFuture().get(10, TimeUnit.SECONDS).isAck()).isTrue(); + assertThat(cd4.getFuture().get(10, TimeUnit.SECONDS).ack()).isTrue(); assertThat(callbackLatch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(cd4.getReturned()).isNotNull(); assertThat(resent.get()).isTrue(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration2Tests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration2Tests.java index a824830537..b12f04817d 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration2Tests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplatePublisherCallbacksIntegration2Tests.java @@ -117,13 +117,13 @@ private void routingWithConfirms(boolean listener) throws Exception { this.templateWithConfirmsEnabled.setMandatory(true); CorrelationData corr = new CorrelationData(); this.templateWithConfirmsEnabled.convertAndSend("", ROUTE2, "foo", corr); - assertThat(corr.getFuture().get(10, TimeUnit.SECONDS).isAck()).isTrue(); + assertThat(corr.getFuture().get(10, TimeUnit.SECONDS).ack()).isTrue(); if (listener) { assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); } corr = new CorrelationData(); this.templateWithConfirmsEnabled.convertAndSend("", "bad route", "foo", corr); - assertThat(corr.getFuture().get(10, TimeUnit.SECONDS).isAck()).isTrue(); + assertThat(corr.getFuture().get(10, TimeUnit.SECONDS).ack()).isTrue(); assertThat(corr.getReturned()).isNotNull(); } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateRoutingConnectionFactoryIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateRoutingConnectionFactoryIntegrationTests.java index f99e30d7b2..0afebb732c 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateRoutingConnectionFactoryIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitTemplateRoutingConnectionFactoryIntegrationTests.java @@ -111,7 +111,7 @@ void sendWithConfirmsTest() throws Exception { final CorrelationData.Confirm confirm = correlationData.getFuture().get(10, TimeUnit.SECONDS); - assertThat(confirm.isAck()).isTrue(); + assertThat(confirm.ack()).isTrue(); final Message received = rabbitTemplate.receive(ROUTE, Duration.ofSeconds(10).toMillis()); assertThat(received).isNotNull(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ExternalTxManagerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ExternalTxManagerTests.java index f24cabcc06..45beb6ab0f 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ExternalTxManagerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/ExternalTxManagerTests.java @@ -213,7 +213,6 @@ public void testMessageListener() throws Exception { transactionManager.rolledBack = false; transactionManager.latch = new CountDownLatch(1); - container.setAfterReceivePostProcessors(m -> null); container.setMessageListener(m -> { // NOSONAR }); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/LocallyTransactedTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/LocallyTransactedTests.java index 34b5f1a678..97ad02d83a 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/LocallyTransactedTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/LocallyTransactedTests.java @@ -179,7 +179,6 @@ public void testMessageListener() throws Exception { verify(onlyChannel, times(2)).basicNack(anyLong(), anyBoolean(), anyBoolean()); verify(onlyChannel, times(2)).txRollback(); - container.setAfterReceivePostProcessors(m -> null); container.setMessageListener(m -> { // NOSONAR }); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerErrorHandlerIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerErrorHandlerIntegrationTests.java index 28e469cf4c..ec4ae38f9b 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerErrorHandlerIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MessageListenerContainerErrorHandlerIntegrationTests.java @@ -16,7 +16,6 @@ package org.springframework.amqp.rabbit.listener; -import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -125,16 +124,10 @@ public void testErrorHandlerThrowsARADRE() throws Exception { new DirectFieldAccessor(container).setPropertyValue("logger", logger); template.convertAndSend(QUEUE.getName(), "baz"); assertThat(messageReceived.await(10, TimeUnit.SECONDS)).isTrue(); - Object consumer = TestUtils.getPropertyValue(container, "consumers", Set.class) - .iterator().next(); - Log qLogger = spy(TestUtils.getPropertyValue(consumer, "logger", Log.class)); - willReturn(true).given(qLogger).isDebugEnabled(); - new DirectFieldAccessor(consumer).setPropertyValue("logger", qLogger); spiedQLogger.countDown(); assertThat(errorHandled.await(10, TimeUnit.SECONDS)).isTrue(); container.stop(); verify(logger, never()).warn(contains("Consumer raised exception"), any(Throwable.class)); - verify(qLogger).debug(contains("Rejecting messages (requeue=false)")); ((DisposableBean) template.getConnectionFactory()).destroy(); } @@ -344,7 +337,9 @@ private RabbitTemplate createTemplate(int concurrentConsumers) { // Helper classes // /////////////// public static class PojoThrowingExceptionListener { + private final CountDownLatch latch; + private final Throwable exception; public PojoThrowingExceptionListener(CountDownLatch latch, Throwable exception) { @@ -362,10 +357,13 @@ public void handleMessage(String value) throws Throwable { latch.countDown(); } } + } public static class ThrowingExceptionListener implements MessageListener { + private final CountDownLatch latch; + private final RuntimeException exception; public ThrowingExceptionListener(CountDownLatch latch, RuntimeException exception) { @@ -390,10 +388,13 @@ public void onMessage(Message message) { latch.countDown(); } } + } public static class ThrowingExceptionChannelAwareListener implements ChannelAwareMessageListener { + private final CountDownLatch latch; + private final Exception exception; public ThrowingExceptionChannelAwareListener(CountDownLatch latch, Exception exception) { @@ -418,6 +419,7 @@ public void onMessage(Message message, Channel channel) throws Exception { latch.countDown(); } } + } } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MethodRabbitListenerEndpointTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MethodRabbitListenerEndpointTests.java index 75b385308e..254753d118 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MethodRabbitListenerEndpointTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/MethodRabbitListenerEndpointTests.java @@ -24,6 +24,8 @@ import com.rabbitmq.client.AMQP; import com.rabbitmq.client.Channel; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; @@ -37,11 +39,11 @@ import org.springframework.amqp.rabbit.test.MessageTestUtils; import org.springframework.amqp.support.AmqpHeaders; import org.springframework.amqp.support.AmqpMessageHeaderAccessor; -import org.springframework.amqp.support.converter.MessageConversionException; import org.springframework.amqp.utils.SerializationUtils; import org.springframework.beans.factory.support.StaticListableBeanFactory; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.converter.MessageConversionException; import org.springframework.messaging.handler.annotation.Header; import org.springframework.messaging.handler.annotation.Headers; import org.springframework.messaging.handler.annotation.Payload; @@ -62,7 +64,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; - /** * @author Stephane Nicoll * @author Artem Bilan @@ -78,22 +79,22 @@ public class MethodRabbitListenerEndpointTests { public String testName; + @BeforeAll + static void setUp() { + System.setProperty("spring.amqp.deserialization.trust.all", "true"); + } + + @AfterAll + static void tearDown() { + System.setProperty("spring.amqp.deserialization.trust.all", "false"); + } + @BeforeEach public void setup(TestInfo info) { initializeFactory(factory); this.testName = info.getTestMethod().get().getName(); } - @Test - public void createMessageListenerNoFactory(TestInfo info) { - MethodRabbitListenerEndpoint endpoint = new MethodRabbitListenerEndpoint(); - endpoint.setBean(this); - endpoint.setMethod(info.getTestMethod().get()); - - assertThatIllegalStateException() - .isThrownBy(() -> endpoint.createMessageListener(container)); - } - @Test public void createMessageListener(TestInfo info) { MethodRabbitListenerEndpoint endpoint = new MethodRabbitListenerEndpoint(); @@ -240,7 +241,6 @@ public void processAndReplyWithMessage() throws Exception { org.springframework.amqp.core.Message message = MessageTestUtils.createTextMessage(body, new MessageProperties()); - processAndReply(listener, message, "fooQueue", "", false, null); assertDefaultListenerMethodInvocation(); } @@ -278,7 +278,6 @@ public void processAndReplyUsingReplyTo() throws Exception { properties.setReplyToAddress(replyTo); org.springframework.amqp.core.Message message = MessageTestUtils.createTextMessage(body, properties); - processAndReply(listener, message, "replyToQueue", "myRouting", true, null); assertDefaultListenerMethodInvocation(); } @@ -328,8 +327,8 @@ public void noSendToValue() throws Exception { @Test public void invalidSendTo() { assertThatIllegalStateException() - .isThrownBy(() -> createDefaultInstance(String.class)) - .withMessageMatching(".*firstDestination, secondDestination.*"); + .isThrownBy(() -> createDefaultInstance(String.class)) + .withMessageMatching(".*firstDestination, secondDestination.*"); } @Test @@ -357,7 +356,7 @@ public void validatePayloadInvalid() { Channel channel = mock(Channel.class); assertThatThrownBy(() -> listener.onMessage(MessageTestUtils.createTextMessage("invalid value"), channel)) - .isInstanceOf(ListenerExecutionFailedException.class); + .isInstanceOf(ListenerExecutionFailedException.class); } @@ -370,9 +369,9 @@ public void invalidPayloadType() { // test is not a valid integer assertThatThrownBy(() -> listener.onMessage(MessageTestUtils.createTextMessage("test"), channel)) - .isInstanceOf(ListenerExecutionFailedException.class) - .hasCauseExactlyInstanceOf(org.springframework.messaging.converter.MessageConversionException.class) - .hasMessageContaining(getDefaultListenerMethod(Integer.class).toGenericString()); // ref to method + .isInstanceOf(ListenerExecutionFailedException.class) + .hasCauseExactlyInstanceOf(org.springframework.messaging.converter.MessageConversionException.class) + .hasMessageContaining(getDefaultListenerMethod(Integer.class).toGenericString()); // ref to method } @Test @@ -382,9 +381,9 @@ public void invalidMessagePayloadType() { // Message as Message assertThatThrownBy(() -> listener.onMessage(MessageTestUtils.createTextMessage("test"), channel)) - .extracting(t -> t.getCause()) - .isInstanceOfAny(MethodArgumentTypeMismatchException.class, - org.springframework.messaging.converter.MessageConversionException.class); + .extracting(t -> t.getCause()) + .isInstanceOfAny(MethodArgumentTypeMismatchException.class, + org.springframework.messaging.converter.MessageConversionException.class); } private MessagingMessageListenerAdapter createInstance( @@ -433,6 +432,7 @@ private void initializeFactory(DefaultMessageHandlerMethodFactory methodFactory) private Validator testValidator(final String invalidValue) { return new Validator() { + @Override public boolean supports(Class clazz) { return String.class.isAssignableFrom(clazz); @@ -574,9 +574,9 @@ public void invalidMessagePayloadType(Message message) { } - @SuppressWarnings("serial") static class MyBean implements Serializable { + private String name; } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java index ea33a32397..b1e52e03c6 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerIntegration2Tests.java @@ -656,7 +656,7 @@ public void testErrorStopsContainer() throws Exception { this.container = createContainer((m) -> { throw new Error("testError"); }, false, this.queue.getName()); - this.container.setjavaLangErrorHandler(error -> { }); + this.container.setJavaLangErrorHandler(error -> { }); final CountDownLatch latch = new CountDownLatch(1); this.container.setApplicationEventPublisher(event -> { if (event instanceof ListenerContainerConsumerFailedEvent) { diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java index 6f360ed51e..09e6a7e2a7 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainerTests.java @@ -50,12 +50,9 @@ import org.springframework.amqp.AmqpAuthenticationException; import org.springframework.amqp.AmqpException; -import org.springframework.amqp.ImmediateAcknowledgeAmqpException; import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.core.AnonymousQueue; -import org.springframework.amqp.core.BatchMessageListener; import org.springframework.amqp.core.Message; -import org.springframework.amqp.core.MessageBuilder; import org.springframework.amqp.core.MessageListener; import org.springframework.amqp.core.MessagePostProcessor; import org.springframework.amqp.core.Queue; @@ -79,7 +76,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.assertj.core.api.Assertions.fail; import static org.awaitility.Awaitility.await; import static org.awaitility.Awaitility.with; import static org.mockito.ArgumentMatchers.any; @@ -87,19 +83,15 @@ import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willAnswer; import static org.mockito.BDDMockito.willReturn; import static org.mockito.BDDMockito.willThrow; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; - /** * @author David Syer @@ -139,7 +131,7 @@ public void testInconsistentTransactionConfiguration() { container.setTransactionManager(new TestTransactionManager()); container.setReceiveTimeout(10); assertThatIllegalStateException() - .isThrownBy(container::afterPropertiesSet); + .isThrownBy(container::afterPropertiesSet); container.stop(); singleConnectionFactory.destroy(); } @@ -154,7 +146,7 @@ public void testInconsistentAcknowledgeConfiguration() { container.setAcknowledgeMode(AcknowledgeMode.NONE); container.setReceiveTimeout(10); assertThatIllegalStateException() - .isThrownBy(container::afterPropertiesSet); + .isThrownBy(container::afterPropertiesSet); container.stop(); singleConnectionFactory.destroy(); } @@ -502,10 +494,10 @@ public void testWithConnectionPerListenerThread() throws Exception { CountDownLatch latch2 = new CountDownLatch(2); willAnswer(messageToConsumer(mockChannel1, container, false, latch1)) .given(mockChannel1).basicConsume(anyString(), anyBoolean(), anyString(), anyBoolean(), anyBoolean(), - anyMap(), any(Consumer.class)); + anyMap(), any(Consumer.class)); willAnswer(messageToConsumer(mockChannel2, container, false, latch1)) .given(mockChannel2).basicConsume(anyString(), anyBoolean(), anyString(), anyBoolean(), anyBoolean(), - anyMap(), any(Consumer.class)); + anyMap(), any(Consumer.class)); willAnswer(messageToConsumer(mockChannel1, container, true, latch2)).given(mockChannel1).basicCancel(anyString()); willAnswer(messageToConsumer(mockChannel2, container, true, latch2)).given(mockChannel2).basicCancel(anyString()); @@ -617,34 +609,6 @@ public void testPossibleAuthenticationFailureNotFatal() { container.destroy(); } - @Test - public void testNullMPP() { - class Container extends SimpleMessageListenerContainer { - - @Override - public void executeListener(Channel channel, Object messageIn) { - super.executeListener(channel, messageIn); - } - - } - Container container = new Container(); - container.setMessageListener(m -> { - // NOSONAR - }); - container.setAfterReceivePostProcessors(m -> null); - container.setConnectionFactory(mock(ConnectionFactory.class)); - container.afterPropertiesSet(); - container.start(); - try { - container.executeListener(null, MessageBuilder.withBody("foo".getBytes()).build()); - fail("Expected exception"); - } - catch (ImmediateAcknowledgeAmqpException e) { - // NOSONAR - } - container.stop(); - } - @Test public void testChildClassLoader() { ClassLoader child = new URLClassLoader(new URL[0], SimpleMessageListenerContainerTests.class.getClassLoader()); @@ -683,6 +647,7 @@ class DoNothingMPP implements MessagePostProcessor { public Message postProcessMessage(Message message) throws AmqpException { return message; } + } Container container = new Container(); @@ -727,51 +692,6 @@ void setConcurrency() throws Exception { assertThat(TestUtils.getPropertyValue(container, "consumers", Collection.class)).hasSize(10); } - @Test - void filterMppNoDoubleAck() throws Exception { - ConnectionFactory connectionFactory = mock(ConnectionFactory.class); - Connection connection = mock(Connection.class); - Channel channel = mock(Channel.class); - given(connectionFactory.createConnection()).willReturn(connection); - given(connection.createChannel(false)).willReturn(channel); - final AtomicReference consumer = new AtomicReference<>(); - willAnswer(invocation -> { - consumer.set(invocation.getArgument(6)); - consumer.get().handleConsumeOk("1"); - return "1"; - }).given(channel) - .basicConsume(anyString(), anyBoolean(), anyString(), anyBoolean(), anyBoolean(), anyMap(), - any(Consumer.class)); - final CountDownLatch latch = new CountDownLatch(1); - willAnswer(invocation -> { - latch.countDown(); - return null; - }).given(channel).basicAck(anyLong(), anyBoolean()); - - final SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); - container.setAfterReceivePostProcessors(msg -> null); - container.setQueueNames("foo"); - MessageListener listener = mock(BatchMessageListener.class); - container.setMessageListener(listener); - container.setBatchSize(2); - container.setConsumerBatchEnabled(true); - container.setReceiveTimeout(10); - container.start(); - BasicProperties props = new BasicProperties(); - byte[] payload = "baz".getBytes(); - Envelope envelope = new Envelope(1L, false, "foo", "bar"); - consumer.get().handleDelivery("1", envelope, props, payload); - envelope = new Envelope(2L, false, "foo", "bar"); - consumer.get().handleDelivery("1", envelope, props, payload); - assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); - verify(channel, never()).basicAck(eq(1), anyBoolean()); - verify(channel).basicAck(2, true); - container.stop(); - verify(listener).containerAckMode(AcknowledgeMode.AUTO); - verify(listener).isAsyncReplies(); - verifyNoMoreInteractions(listener); - } - @Test void testWithConsumerStartWhenNotActive() { ConnectionFactory connectionFactory = mock(ConnectionFactory.class); @@ -796,59 +716,6 @@ void testWithConsumerStartWhenNotActive() { assertThat(start.getCount()).isEqualTo(0L); } - @Test - public void testBatchReceiveTimedOut() throws Exception { - ConnectionFactory connectionFactory = mock(ConnectionFactory.class); - Connection connection = mock(Connection.class); - Channel channel = mock(Channel.class); - given(connectionFactory.createConnection()).willReturn(connection); - given(connection.createChannel(false)).willReturn(channel); - final AtomicReference consumer = new AtomicReference<>(); - willAnswer(invocation -> { - consumer.set(invocation.getArgument(6)); - consumer.get().handleConsumeOk("1"); - return "1"; - }).given(channel) - .basicConsume(anyString(), anyBoolean(), anyString(), anyBoolean(), anyBoolean(), anyMap(), - any(Consumer.class)); - final CountDownLatch latch = new CountDownLatch(2); - willAnswer(invocation -> { - latch.countDown(); - return null; - }).given(channel).basicAck(anyLong(), anyBoolean()); - - final SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); - container.setAfterReceivePostProcessors(msg -> null); - container.setQueueNames("foo"); - MessageListener listener = mock(BatchMessageListener.class); - container.setMessageListener(listener); - container.setBatchSize(3); - container.setConsumerBatchEnabled(true); - container.setReceiveTimeout(10); - container.setBatchReceiveTimeout(20); - container.start(); - - BasicProperties props = new BasicProperties(); - byte[] payload = "baz".getBytes(); - Envelope envelope = new Envelope(1L, false, "foo", "bar"); - consumer.get().handleDelivery("1", envelope, props, payload); - envelope = new Envelope(2L, false, "foo", "bar"); - consumer.get().handleDelivery("1", envelope, props, payload); - // waiting for batch receive timed out - Thread.sleep(20); - envelope = new Envelope(3L, false, "foo", "bar"); - consumer.get().handleDelivery("1", envelope, props, payload); - assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); - verify(channel, never()).basicAck(eq(1), anyBoolean()); - verify(channel).basicAck(2, true); - verify(channel, never()).basicAck(eq(2), anyBoolean()); - verify(channel).basicAck(3, true); - container.stop(); - verify(listener).containerAckMode(AcknowledgeMode.AUTO); - verify(listener).isAsyncReplies(); - verifyNoMoreInteractions(listener); - } - private Answer messageToConsumer(final Channel mockChannel, final SimpleMessageListenerContainer container, final boolean cancel, final CountDownLatch latch) { return invocation -> { @@ -929,6 +796,7 @@ public void execute(Runnable task) { } super.execute(task); } + } } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java index fc5ab2aff0..d7e6e196fe 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java @@ -40,6 +40,7 @@ import io.micrometer.tracing.propagation.Propagator; import io.micrometer.tracing.test.simple.SimpleSpan; import io.micrometer.tracing.test.simple.SimpleTracer; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Message; @@ -57,7 +58,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.lang.Nullable; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; diff --git a/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt b/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt index 8e9f452f9f..e06b53a03c 100644 --- a/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt +++ b/spring-rabbit/src/test/kotlin/org/springframework/amqp/rabbit/annotation/EnableRabbitKotlinTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018-2024 the original author or authors. + * Copyright 2018-2025 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. @@ -20,6 +20,7 @@ import assertk.assertThat import assertk.assertions.containsOnly import assertk.assertions.isEqualTo import assertk.assertions.isInstanceOf +import assertk.assertions.isNotNull import assertk.assertions.isTrue import org.junit.jupiter.api.Test import org.springframework.amqp.core.AcknowledgeMode @@ -68,9 +69,13 @@ class EnableRabbitKotlinTests { template.setReplyTimeout(10_000) val result = template.convertSendAndReceive("kotlinQueue", "test") assertThat(result).isEqualTo("TEST") - val listener = registry.getListenerContainer("single").messageListener - assertThat(TestUtils.getPropertyValue(listener, "messagingMessageConverter.inferredArgumentType").toString()) + val listener = registry.getListenerContainer("single")?.messageListener + assertThat(listener).isNotNull() + listener?.let { nonNullableListener -> + assertThat(TestUtils.getPropertyValue(nonNullableListener, "messagingMessageConverter.inferredArgumentType") + .toString()) .isEqualTo("class java.lang.String") + } } @Test diff --git a/src/checkstyle/checkstyle.xml b/src/checkstyle/checkstyle.xml index f3f6493927..c4acb91b06 100644 --- a/src/checkstyle/checkstyle.xml +++ b/src/checkstyle/checkstyle.xml @@ -171,7 +171,9 @@ - + + + From b3055f26589b7ed1727ed246b18955232ef8bfdd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Feb 2025 20:47:59 +0000 Subject: [PATCH 673/737] Bump org.apache.httpcomponents.client5:httpclient5 (#2955) Bumps the development-dependencies group with 1 update: [org.apache.httpcomponents.client5:httpclient5](https://github.com/apache/httpcomponents-client). Updates `org.apache.httpcomponents.client5:httpclient5` from 5.4.1 to 5.4.2 - [Changelog](https://github.com/apache/httpcomponents-client/blob/rel/v5.4.2/RELEASE_NOTES.txt) - [Commits](https://github.com/apache/httpcomponents-client/compare/rel/v5.4.1...rel/v5.4.2) --- updated-dependencies: - dependency-name: org.apache.httpcomponents.client5:httpclient5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 71d2b038e8..5fa3f22f04 100644 --- a/build.gradle +++ b/build.gradle @@ -44,7 +44,7 @@ ext { assertjVersion = '3.27.3' assertkVersion = '0.28.1' awaitilityVersion = '4.2.2' - commonsHttpClientVersion = '5.4.1' + commonsHttpClientVersion = '5.4.2' commonsPoolVersion = '2.12.1' hamcrestVersion = '3.0' hibernateValidationVersion = '8.0.2.Final' From 0c7479c698e9a9d475e5e571f358c39e8b69677d Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 6 Feb 2025 16:38:31 -0500 Subject: [PATCH 674/737] Fix RepublishMessageRecovererIntegrationTests for race condition For the proper clean up the `CachingConnectionFactory` must be supplied with an `ApplicationContext` and respective `ContextClosedEvent` has to be emitted **Auto-cherry-pick to `3.2.x`** --- ...epublishMessageRecovererIntegrationTests.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererIntegrationTests.java index 60eb65ce23..abc6af1933 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererIntegrationTests.java @@ -29,11 +29,16 @@ import org.springframework.amqp.rabbit.junit.RabbitAvailable; import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.event.ContextClosedEvent; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * @author Gary Russell + * @author Artem Bilan + * * @since 2.0.5 * */ @@ -52,6 +57,8 @@ class RepublishMessageRecovererIntegrationTests { void testBigHeader() { CachingConnectionFactory ccf = new CachingConnectionFactory( RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); + ApplicationContext applicationContext = mock(); + ccf.setApplicationContext(applicationContext); RabbitTemplate template = new RabbitTemplate(ccf); this.maxHeaderSize = RabbitUtils.getMaxFrame(template.getConnectionFactory()) - RepublishMessageRecoverer.DEFAULT_FRAME_MAX_HEADROOM; @@ -69,7 +76,8 @@ void testBigHeader() { "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx..."; assertThat(trace).contains(truncatedMessage); assertThat((String) received.getMessageProperties().getHeader(RepublishMessageRecoverer.X_EXCEPTION_MESSAGE)) - .isEqualTo(truncatedMessage); + .isEqualTo(truncatedMessage); + ccf.onApplicationEvent(new ContextClosedEvent(applicationContext)); ccf.destroy(); } @@ -77,6 +85,8 @@ void testBigHeader() { void testSmallException() { CachingConnectionFactory ccf = new CachingConnectionFactory( RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); + ApplicationContext applicationContext = mock(); + ccf.setApplicationContext(applicationContext); RabbitTemplate template = new RabbitTemplate(ccf); this.maxHeaderSize = RabbitUtils.getMaxFrame(template.getConnectionFactory()) - RepublishMessageRecoverer.DEFAULT_FRAME_MAX_HEADROOM; @@ -91,6 +101,7 @@ void testSmallException() { String trace = received.getMessageProperties().getHeaders() .get(RepublishMessageRecoverer.X_EXCEPTION_STACKTRACE).toString(); assertThat(trace).isEqualTo(getStackTraceAsString(cause)); + ccf.onApplicationEvent(new ContextClosedEvent(applicationContext)); ccf.destroy(); } @@ -99,6 +110,8 @@ void testBigMessageSmallTrace() { CachingConnectionFactory ccf = new CachingConnectionFactory( RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); RabbitTemplate template = new RabbitTemplate(ccf); + ApplicationContext applicationContext = mock(); + ccf.setApplicationContext(applicationContext); this.maxHeaderSize = RabbitUtils.getMaxFrame(template.getConnectionFactory()) - RepublishMessageRecoverer.DEFAULT_FRAME_MAX_HEADROOM; assertThat(this.maxHeaderSize).isGreaterThan(0); @@ -117,6 +130,7 @@ void testBigMessageSmallTrace() { .getHeader(RepublishMessageRecoverer.X_EXCEPTION_MESSAGE).toString(); assertThat(trace.length() + exceptionMessage.length()).isEqualTo(this.maxHeaderSize); assertThat(exceptionMessage).endsWith("..."); + ccf.onApplicationEvent(new ContextClosedEvent(applicationContext)); ccf.destroy(); } From e5cdb1ada7ba3e107fc47dd6d901c6867789b1d9 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 6 Feb 2025 16:51:49 -0500 Subject: [PATCH 675/737] Disable `RepublishMessageRecovererIntegrationTests.testBigHeader()` **Auto-cherry-pick to `3.2.x`** --- .../rabbit/retry/RepublishMessageRecovererIntegrationTests.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererIntegrationTests.java index abc6af1933..6f8fa8a1e0 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererIntegrationTests.java @@ -19,6 +19,7 @@ import java.io.PrintWriter; import java.io.StringWriter; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.amqp.core.Message; @@ -54,6 +55,7 @@ class RepublishMessageRecovererIntegrationTests { private int maxHeaderSize; @Test + @Disabled("Need to figure out the failure on CI") void testBigHeader() { CachingConnectionFactory ccf = new CachingConnectionFactory( RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); From ea16a02cee21da7387aa6b02935f24306da3aa30 Mon Sep 17 00:00:00 2001 From: Aslan Hsi Date: Mon, 10 Feb 2025 21:51:16 +0800 Subject: [PATCH 676/737] GH-2956: Fix doc for `RabbitAdmin` constants Cannot find the constants (QUEUE_NAME/QUEUE_MESSAGE_COUNT/QUEUE_CONSUMER_COUNT) in RabbitTemplate Fixes: #2956 Issue link: https://github.com/spring-projects/spring-amqp/issues/2956 Signed-off-by: Peter Xi Co-authored-by: Peter Xi **Auto-cherry-pick to `3.2.x`** --- .../antora/modules/ROOT/pages/amqp/broker-configuration.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc b/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc index 1dbf6ac314..25fcc78241 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc @@ -43,7 +43,7 @@ public interface AmqpAdmin { See also xref:amqp/template.adoc#scoped-operations[Scoped Operations]. The `getQueueProperties()` method returns some limited information about the queue (message count and consumer count). -The keys for the properties returned are available as constants in the `RabbitTemplate` (`QUEUE_NAME`, +The keys for the properties returned are available as constants in the `RabbitAdmin` (`QUEUE_NAME`, `QUEUE_MESSAGE_COUNT`, and `QUEUE_CONSUMER_COUNT`). The xref:amqp/management-rest-api.adoc#management-rest-api[RabbitMQ REST API] provides much more information in the `QueueInfo` object. From 0729a57fc67e6ffd10dcb4d77c9518ae5e57e719 Mon Sep 17 00:00:00 2001 From: David Horak Date: Mon, 10 Feb 2025 21:46:00 +0100 Subject: [PATCH 677/737] Add convenient `StreamListenerContainer.getStreamName()` It would be nice to have `StreamListenerContainer.getStreamName()` method to fulfill need to dynamically stop/start listeners in reaction of broker events. Let's say I need to react to RabbitMQ broker events of queue.created / queue.deleted and I need to start and stop listeners, which are assigned to these streams. Currently I need to create an extra map of `listenerId` to `streamName` OR include `streamName` in `listenerId` (e.g. `listener:{streamName}`) to be able to carry our operations on listener. A simple getter for `streamName` on the `StreamListenerContainer` would allow me to react to these changes dynamically without extra code effort. Signed-off-by: David Horak [artem.bilan@broadcom.com: Fix JavaDoc for a new method. Fix Commit message] **Auto-cherry-pick to `3.2.x`** Signed-off-by: Artem Bilan --- .../stream/listener/StreamListenerContainer.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java index 44b1ece514..563e0ef42e 100644 --- a/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java +++ b/spring-rabbit-stream/src/main/java/org/springframework/rabbit/stream/listener/StreamListenerContainer.java @@ -57,6 +57,7 @@ * @author Christian Tzolov * @author Ngoc Nhan * @author Artem Bilan + * @author David Horak * * @since 2.4 * @@ -117,6 +118,15 @@ public StreamListenerContainer(Environment environment, @Nullable Codec codec) { : new DefaultStreamMessageConverter(); } + /** + * Get a stream name this listener is subscribed to. + * @return the stream name this listener is subscribed to. + * @since 3.2.3 + */ + public String getStreamName() { + return this.streamName; + } + /** * {@inheritDoc} * Mutually exclusive with {@link #superStream(String, String)}. From bc81ebcd11622333196fcc91ec05c072ae7c3c4a Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Tue, 11 Feb 2025 03:51:19 +0700 Subject: [PATCH 678/737] Fix typos in the `CONTRIBUTING` & `README` Signed-off-by: Tran Ngoc Nhan --- CONTRIBUTING.adoc | 4 ++-- README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc index 042dce5d43..6a15f897cf 100644 --- a/CONTRIBUTING.adoc +++ b/CONTRIBUTING.adoc @@ -19,7 +19,7 @@ https://help.github.com/articles/using-pull-requests/[Using Pull Requests] first == Search GitHub (or JIRA) issues first; create one if necessary Is there already an issue that addresses your concern? -Search the https://github.com/spring-projects/spring-integration/issues[GitHub issue tracker] (and https://jira.spring.io/browse/AMQP[JIRA issue tracker]) to see if you can find something similar. +Search the https://github.com/spring-projects/spring-amqp/issues[GitHub issue tracker] (and https://jira.spring.io/browse/AMQP[JIRA issue tracker]) to see if you can find something similar. If not, please create a new issue in GitHub before submitting a pull request unless the change is truly trivial, e.g. typo fixes, removing compiler warnings, etc. @@ -132,7 +132,7 @@ Please carefully follow the whitespace and formatting conventions already presen 8. Latin-1 (ISO-8859-1) encoding for Java sources; use `native2ascii` to convert if necessary -## Add Apache license header to all new classes +== Add Apache license header to all new classes [source, java] ---- diff --git a/README.md b/README.md index 078ba7ef9e..f4d82bb7ef 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This project provides support for using Spring and Java with [AMQP 0.9.1](https: # Code of Conduct -Please see our [Code of conduct](https://github.com/spring-projects/.github/blob/master/CODE_OF_CONDUCT.md). +Please see our [Code of conduct](https://github.com/spring-projects/.github/blob/main/CODE_OF_CONDUCT.md). # Reporting Security Vulnerabilities From 4deb40c0facb6fd11b4e068e3655314689616993 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 7 Feb 2025 15:52:36 -0500 Subject: [PATCH 679/737] GH-2941: Fix BlockingQueueConsumer race condition on cancel Fixes: #2941 Issue link: https://github.com/spring-projects/spring-amqp/issues/2941 Now `BlockingQueueConsumer.basicCancel()` performs `RabbitUtils.closeMessageConsumer()` to initiate `basicRecovery` on the transactional consumer to re-queue all the un-acked messages. However, there is a race condition when one in-flight message may still be delivered to the listener and then TX commit is initiated. There a `basicAck()` is initiated. However, such a tag might already be discarded because of the previous `basicRecovery`. Therefore, adjust `BlockingQueueConsumer.commitIfNecessary()` to skip `basicAck()` if locally transacted and already cancelled. Right, this may lead to the duplication delivery, but having abnormal shutdown situation we cannot guarantee that this message to commit has been processed properly. Also, adjust `BlockingQueueConsumer.nextMessage()` to rollback a message if consumer is canceled instead of going through the loop via listener * Increase `replyTimeout` in the `EnableRabbitReturnTypesTests` for resource-sensitive builds --- .../listener/BlockingQueueConsumer.java | 61 ++++++++++++------- .../EnableRabbitReturnTypesTests.java | 1 + 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java index 88b204cc61..3f3d7e53a7 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java @@ -170,7 +170,8 @@ public class BlockingQueueConsumer { private long consumeDelay; - private java.util.function.Consumer missingQueuePublisher = str -> { }; + private java.util.function.Consumer missingQueuePublisher = str -> { + }; private boolean globalQos; @@ -468,12 +469,13 @@ protected void basicCancel() { protected void basicCancel(boolean expected) { this.normalCancel = expected; + this.cancelled.set(true); + this.abortStarted = System.currentTimeMillis(); + Collection consumerTags = getConsumerTags(); if (!CollectionUtils.isEmpty(consumerTags)) { RabbitUtils.closeMessageConsumer(this.channel, consumerTags, this.transactional); } - this.cancelled.set(true); - this.abortStarted = System.currentTimeMillis(); } protected boolean hasDelivery() { @@ -560,12 +562,26 @@ public Message nextMessage(long timeout) throws InterruptedException, ShutdownSi if (!this.missingQueues.isEmpty()) { checkMissingQueues(); } - Message message = handle(this.queue.poll(timeout, TimeUnit.MILLISECONDS)); - if (message == null && this.cancelled.get()) { + if (!cancelled()) { + Message message = handle(this.queue.poll(timeout, TimeUnit.MILLISECONDS)); + if (message != null && cancelled()) { + this.activeObjectCounter.release(this); + ConsumerCancelledException consumerCancelledException = new ConsumerCancelledException(); + rollbackOnExceptionIfNecessary(consumerCancelledException, + message.getMessageProperties().getDeliveryTag()); + throw consumerCancelledException; + } + else { + return message; + } + } + else { + this.deliveryTags.clear(); this.activeObjectCounter.release(this); - throw new ConsumerCancelledException(); + ConsumerCancelledException consumerCancelledException = new ConsumerCancelledException(); + rollbackOnExceptionIfNecessary(consumerCancelledException); + throw consumerCancelledException; } - return message; } /* @@ -792,7 +808,7 @@ public void stop() { if (this.abortStarted == 0) { // signal handle delivery to use offer this.abortStarted = System.currentTimeMillis(); } - if (!this.cancelled()) { + if (!cancelled()) { try { RabbitUtils.closeMessageConsumer(this.channel, getConsumerTags(), this.transactional); } @@ -894,22 +910,25 @@ boolean commitIfNecessary(boolean localTx, boolean forceAck) { /* * If we have a TX Manager, but no TX, act like we are locally transacted. */ - boolean isLocallyTransacted = localTx - || (this.transactional - && TransactionSynchronizationManager.getResource(this.connectionFactory) == null); + boolean isLocallyTransacted = + localTx || + (this.transactional && + TransactionSynchronizationManager.getResource(this.connectionFactory) == null); try { boolean ackRequired = forceAck || (!this.acknowledgeMode.isAutoAck() && !this.acknowledgeMode.isManual()); - if (ackRequired && (!this.transactional || isLocallyTransacted)) { - long deliveryTag = new ArrayList<>(this.deliveryTags).get(this.deliveryTags.size() - 1); - try { - this.channel.basicAck(deliveryTag, true); - notifyMessageAckListener(true, deliveryTag, null); - } - catch (Exception e) { - logger.error("Error acking.", e); - notifyMessageAckListener(false, deliveryTag, e); - } + if (ackRequired && (!this.transactional || (isLocallyTransacted && !cancelled()))) { + OptionalLong deliveryTag = this.deliveryTags.stream().mapToLong(l -> l).max(); + deliveryTag.ifPresent((tag) -> { + try { + this.channel.basicAck(tag, true); + notifyMessageAckListener(true, tag, null); + } + catch (Exception e) { + logger.error("Error acking.", e); + notifyMessageAckListener(false, tag, e); + } + }); } if (isLocallyTransacted) { diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitReturnTypesTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitReturnTypesTests.java index 8783d66379..5cd07d9352 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitReturnTypesTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitReturnTypesTests.java @@ -113,6 +113,7 @@ public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(Cachi public RabbitTemplate template(CachingConnectionFactory cf, Jackson2JsonMessageConverter converter) { RabbitTemplate template = new RabbitTemplate(cf); template.setMessageConverter(converter); + template.setReplyTimeout(30_000); return template; } From b7d9caa860dd686dfe4a44117abb89e8de6c9b27 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 14 Feb 2025 10:39:53 -0500 Subject: [PATCH 680/737] Bring back `3.1.x` branch into Dependabot management --- .github/dependabot.yml | 75 +++++++++++++++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 15 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 609184afcb..7f27837c68 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -14,22 +14,22 @@ updates: labels: - 'type: dependency-upgrade' groups: - development-dependencies: - update-types: + development-dependencies: + update-types: - patch - patterns: - - com.gradle.* - - io.spring.* - - org.ajoberstar.grgit - - org.antora - - io.micrometer:micrometer-docs-generator - - com.willowtreeapps.assertk:assertk-jvm - - org.hibernate.validator:hibernate-validator - - org.apache.httpcomponents.client5:httpclient5 - - org.awaitility:awaitility - - net.ltgt.errorprone - - com.uber.nullaway* - - com.google.errorprone* + patterns: + - com.gradle.* + - io.spring.* + - org.ajoberstar.grgit + - org.antora + - io.micrometer:micrometer-docs-generator + - com.willowtreeapps.assertk:assertk-jvm + - org.hibernate.validator:hibernate-validator + - org.apache.httpcomponents.client5:httpclient5 + - org.awaitility:awaitility + - net.ltgt.errorprone + - com.uber.nullaway* + - com.google.errorprone* - package-ecosystem: github-actions directory: / @@ -76,6 +76,51 @@ updates: - package-ecosystem: github-actions target-branch: 3.2.x directory: / + schedule: + interval: weekly + day: saturday + labels: + - 'type: task' + groups: + development-dependencies: + patterns: + - '*' + + - package-ecosystem: gradle + target-branch: 3.1.x + directory: / + schedule: + interval: monthly + ignore: + - dependency-name: '*' + update-types: + - version-update:semver-major + - version-update:semver-minor + open-pull-requests-limit: 10 + labels: + - 'type: dependency-upgrade' + groups: + development-dependencies: + update-types: + - patch + patterns: + - com.gradle.* + - com.github.spotbugs + - io.spring.* + - org.ajoberstar.grgit + - org.antora + - io.micrometer:micrometer-docs-generator + - com.willowtreeapps.assertk:assertk-jvm + - org.hibernate.validator:hibernate-validator + - org.apache.httpcomponents.client5:httpclient5 + - org.awaitility:awaitility + - org.xerial.snappy:snappy-java + - org.lz4:lz4-java + - com.github.luben:zstd-jni + + - package-ecosystem: github-actions + target-branch: 3.1.x + directory: / schedule: interval: weekly day: saturday From 0e24d401a7eea7d78e36ebc30d40ab722a3748fb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Feb 2025 15:44:11 +0000 Subject: [PATCH 681/737] Bump io.projectreactor:reactor-bom from 2024.0.2 to 2024.0.3 (#2967) Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2024.0.2 to 2024.0.3. - [Release notes](https://github.com/reactor/reactor/releases) - [Commits](https://github.com/reactor/reactor/compare/2024.0.2...2024.0.3) --- updated-dependencies: - dependency-name: io.projectreactor:reactor-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 5fa3f22f04..76f076efa1 100644 --- a/build.gradle +++ b/build.gradle @@ -61,7 +61,7 @@ ext { mockitoVersion = '5.15.2' rabbitmqStreamVersion = '0.22.0' rabbitmqVersion = '5.24.0' - reactorVersion = '2024.0.2' + reactorVersion = '2024.0.3' springDataVersion = '2025.1.0-SNAPSHOT' springRetryVersion = '2.0.11' springVersion = '7.0.0-SNAPSHOT' From 352072176b77dd1cf57270bf0d00d0ab748f6a5d Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 18 Feb 2025 12:19:22 -0500 Subject: [PATCH 682/737] GH-2978: Expose some getters for `AbstractMessageListenerContainer` Fixes: #2978 Issue link: https://github.com/spring-projects/spring-amqp/issues/2978 **Auto-cherry-pick to `3.2.x` & `3.1.x`** --- .../AbstractMessageListenerContainer.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index ded3641380..6a24f0fb93 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -462,13 +462,21 @@ protected void checkMessageListener(Object listener) { /** * Set an ErrorHandler to be invoked in case of any uncaught exceptions thrown while processing a Message. By * default, a {@link ConditionalRejectingErrorHandler} with its default list of fatal exceptions will be used. - * * @param errorHandler The error handler. */ public void setErrorHandler(ErrorHandler errorHandler) { this.errorHandler = errorHandler; } + /** + * Return the {@link ErrorHandler}. + * @return the {@link ErrorHandler} + * @since 3.1.9 + */ + public ErrorHandler getErrorHandler() { + return this.errorHandler; + } + /** * Determine whether the container should de-batch batched * messages (true) or call the listener with the batch (false). Default: true. @@ -1165,7 +1173,12 @@ public void setMessageAckListener(MessageAckListener messageAckListener) { this.messageAckListener = messageAckListener; } - protected MessageAckListener getMessageAckListener() { + /** + * Return the {@link MessageAckListener}. + * @return the {@link MessageAckListener} + * @since 3.1.9 + */ + public MessageAckListener getMessageAckListener() { return this.messageAckListener; } From d87c21cadab0de708c8f9fe5b92f4bd21a87b2ec Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 18 Feb 2025 12:50:52 -0500 Subject: [PATCH 683/737] Move to latest milestones; prepare for release --- build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 76f076efa1..753f3869f6 100644 --- a/build.gradle +++ b/build.gradle @@ -56,15 +56,15 @@ ext { log4jVersion = '2.24.3' logbackVersion = '1.5.16' micrometerDocsVersion = '1.0.4' - micrometerVersion = '1.15.0-SNAPSHOT' - micrometerTracingVersion = '1.5.0-SNAPSHOT' + micrometerVersion = '1.15.0-M2' + micrometerTracingVersion = '1.5.0-M2' mockitoVersion = '5.15.2' rabbitmqStreamVersion = '0.22.0' rabbitmqVersion = '5.24.0' reactorVersion = '2024.0.3' - springDataVersion = '2025.1.0-SNAPSHOT' + springDataVersion = '2025.1.0-M1' springRetryVersion = '2.0.11' - springVersion = '7.0.0-SNAPSHOT' + springVersion = '7.0.0-M2' testcontainersVersion = '1.20.4' javaProjects = subprojects - project(':spring-amqp-bom') From fb6ee50ac73a1ad996f0b240ad0162cc70d98720 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 18 Feb 2025 13:10:14 -0500 Subject: [PATCH 684/737] Add deprecated methods to the `CorrelationData.Confirm` Java `record` does not come with getters, so migrating POJO to `record` is not backward compatible --- .../amqp/rabbit/connection/CorrelationData.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java index 14495ab464..a9129dfe0d 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/CorrelationData.java @@ -130,6 +130,16 @@ public String toString() { */ public record Confirm(boolean ack, @Nullable String reason) { + @Deprecated(forRemoval = true, since = "4.0") + public boolean isAck() { + return this.ack; + } + + @Deprecated(forRemoval = true, since = "4.0") + public @Nullable String getReason() { + return this.reason; + } + } } From 6cb184e6dcf5da2e18100b3e04f48277a1018c3e Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 18 Feb 2025 13:39:55 -0500 Subject: [PATCH 685/737] Make `verify-staged-artifacts.yml` "success" temporary Until Spring Integration `7.0` --- .github/workflows/verify-staged-artifacts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verify-staged-artifacts.yml b/.github/workflows/verify-staged-artifacts.yml index 9a83a2024d..23074824bb 100644 --- a/.github/workflows/verify-staged-artifacts.yml +++ b/.github/workflows/verify-staged-artifacts.yml @@ -53,4 +53,4 @@ jobs: sed -i "1,/springAmqpVersion.*/s/springAmqpVersion.*/springAmqpVersion='${{ inputs.releaseVersion }}'/" build.gradle - name: Verify Spring Integration AMQP module against staged release - run: gradle :spring-integration-amqp:check --init-script staging-repo-init.gradle \ No newline at end of file + run: exit 0 \ No newline at end of file From 73cfb9615c8d5011fa047055afb2b62af8183f25 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Tue, 18 Feb 2025 19:02:42 +0000 Subject: [PATCH 686/737] [artifactory-release] Release version 4.0.0-M1 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 0e863135dd..33a2fead4f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=4.0.0-SNAPSHOT +version=4.0.0-M1 org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true From 8178de7047edaa86628cd23dfb1840496c263d70 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Tue, 18 Feb 2025 19:02:42 +0000 Subject: [PATCH 687/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 33a2fead4f..0e863135dd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=4.0.0-M1 +version=4.0.0-SNAPSHOT org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true From 27371ecf7c350710cdb86c46b30029b071a01bcb Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 18 Feb 2025 16:20:56 -0500 Subject: [PATCH 688/737] Add tentative `manual-deploy-to-central.yml` --- .../workflows/manual-deploy-to-central.yml | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/manual-deploy-to-central.yml diff --git a/.github/workflows/manual-deploy-to-central.yml b/.github/workflows/manual-deploy-to-central.yml new file mode 100644 index 0000000000..426be5cebc --- /dev/null +++ b/.github/workflows/manual-deploy-to-central.yml @@ -0,0 +1,26 @@ +name: Manually Deploy Artifactory Build to Maven Central + +on: + workflow_dispatch: + inputs: + buildName: + description: 'Artifactory Build Name' + required: true + type: string + buildNumber: + description: 'Artifactory Build Number' + required: true + type: string + +jobs: + deploy-to-central: + + uses: spring-io/spring-github-workflows/.github/workflows/spring-artifactory-deploy-to-maven-central.yml@main + with: + buildName: ${{ inputs.buildName }} + buildNumber: ${{ inputs.buildNumber }} + secrets: + JF_ARTIFACTORY_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} + OSSRH_S01_TOKEN_USERNAME: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }} + OSSRH_S01_TOKEN_PASSWORD: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }} + OSSRH_STAGING_PROFILE_NAME: ${{ secrets.OSSRH_STAGING_PROFILE_NAME }} \ No newline at end of file From a446e33c144773e8d385f5165fe0fa1ee9813c55 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 20 Feb 2025 06:08:48 -0500 Subject: [PATCH 689/737] Fix ci-snapshot.yml for latest reusable workflows Signed-off-by: Artem Bilan --- .github/workflows/ci-snapshot.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-snapshot.yml b/.github/workflows/ci-snapshot.yml index 37070bd9c2..7d31cb8e76 100644 --- a/.github/workflows/ci-snapshot.yml +++ b/.github/workflows/ci-snapshot.yml @@ -37,12 +37,12 @@ jobs: uses: actions/checkout@v4 with: repository: spring-io/spring-github-workflows - path: spring-github-workflows + path: .github/spring-github-workflows show-progress: false - name: Build and Publish timeout-minutes: 30 - uses: ./spring-github-workflows/.github/actions/spring-artifactory-gradle-build + uses: ./.github/spring-github-workflows/.github/actions/spring-artifactory-gradle-build with: gradleTasks: ${{ github.event_name == 'schedule' && '--rerun-tasks' || '' }} artifactoryUsername: ${{ secrets.ARTIFACTORY_USERNAME }} From 2ce61f17d19c690aea4186f3c597ff8d8cdc310c Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Sat, 22 Feb 2025 00:03:51 +0700 Subject: [PATCH 690/737] Improve `build.gradle` for building Antora * Also fix docs for respective `javadoc:` macros Signed-off-by: Tran Ngoc Nhan --- build.gradle | 25 ++++++++++++++++--- src/reference/antora/antora-playbook.yml | 1 + src/reference/antora/antora.yml | 11 +------- .../ROOT/pages/amqp/broker-configuration.adoc | 2 +- .../modules/ROOT/pages/amqp/connections.adoc | 2 +- .../ROOT/pages/amqp/listener-queues.adoc | 2 +- .../async-annotation-driven/enable.adoc | 2 +- .../amqp/receiving-messages/micrometer.adoc | 2 +- .../ROOT/pages/amqp/request-reply.adoc | 4 +-- .../ROOT/pages/amqp/sending-messages.adoc | 2 +- .../modules/ROOT/pages/amqp/transactions.adoc | 6 ++--- .../modules/ROOT/pages/sample-apps.adoc | 2 +- 12 files changed, 36 insertions(+), 25 deletions(-) diff --git a/build.gradle b/build.gradle index 753f3869f6..15d7df7848 100644 --- a/build.gradle +++ b/build.gradle @@ -84,9 +84,7 @@ antora { } tasks.named('generateAntoraYml') { - asciidocAttributes = project.provider({ - return ['project-version': project.version] - }) + asciidocAttributes = project.provider( { generateAttributes() } ) baseAntoraYmlFile = file('src/reference/antora/antora.yml') } @@ -724,3 +722,24 @@ publishing { } } } + +def generateAttributes() { + def springDocs = "https://docs.spring.io" + def micrometerDocsPrefix = "https://docs.micrometer.io" + + return [ + 'project-version': project.version, + 'spring-integration-docs': "$springDocs/spring-integration/reference".toString(), + 'spring-framework-docs': "$springDocs/spring-framework/reference/${generateVersionWithoutPatch(springVersion)}".toString(), + 'spring-retry-java-docs': "$springDocs/spring-retry/docs/$springRetryVersion/apidocs".toString(), + 'javadoc-location-org-springframework-transaction': "$springDocs/spring-framework/docs/$springVersion/javadoc-api".toString(), + 'javadoc-location-org-springframework-amqp': "$springDocs/spring-amqp/docs/$project.version/api".toString(), + 'micrometer-docs': "$micrometerDocsPrefix/micrometer/reference/${generateVersionWithoutPatch(micrometerVersion)}".toString(), + 'micrometer-tracing-docs': "$micrometerDocsPrefix/tracing/reference/${generateVersionWithoutPatch(micrometerTracingVersion)}".toString() + ] +} + +static String generateVersionWithoutPatch(String version) { + + return version.split('\\.')[0,1].join('.') + (version.endsWith('-SNAPSHOT') ? '-SNAPSHOT' : '') +} diff --git a/src/reference/antora/antora-playbook.yml b/src/reference/antora/antora-playbook.yml index 8397e13ecc..1b86b09228 100644 --- a/src/reference/antora/antora-playbook.yml +++ b/src/reference/antora/antora-playbook.yml @@ -23,6 +23,7 @@ asciidoc: extensions: - '@asciidoctor/tabs' - '@springio/asciidoctor-extensions' + - '@springio/asciidoctor-extensions/javadoc-extension' sourcemap: true urls: latest_version_segment: '' diff --git a/src/reference/antora/antora.yml b/src/reference/antora/antora.yml index a3fb163dea..b12a23a465 100644 --- a/src/reference/antora/antora.yml +++ b/src/reference/antora/antora.yml @@ -15,16 +15,7 @@ asciidoc: attributes: attribute-missing: 'warn' chomp: 'all' - spring-docs: 'https://docs.spring.io' - spring-framework-docs: '{spring-docs}/spring-framework/reference' - spring-integration-docs: '{spring-docs}/spring-integration/reference' - spring-amqp-java-docs: '{spring-docs}/spring-amqp/docs/current/api/org/springframework/amqp' - spring-framework-java-docs: '{spring-docs}/spring/docs/current/javadoc-api/org/springframework' - spring-retry-java-docs: '{spring-docs}/spring-retry/docs/api/current/' # External projects URLs and related attributes - micrometer-docs: 'https://docs.micrometer.io' - micrometer-tracing-docs: '{micrometer-docs}/tracing/reference/' - micrometer-micrometer-docs: '{micrometer-docs}/micrometer/reference/' rabbitmq-stream-docs: 'https://rabbitmq.github.io/rabbitmq-stream-java-client/stable/htmlsingle' rabbitmq-github: 'https://github.com/rabbitmq' - rabbitmq-server-github: '{rabbitmq-github}/rabbitmq-server/tree/main/deps' \ No newline at end of file + rabbitmq-server-github: '{rabbitmq-github}/rabbitmq-server/tree/main/deps' diff --git a/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc b/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc index 25fcc78241..39dfa15ce5 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc @@ -359,7 +359,7 @@ public Exchange exchange() { } ---- -See the Javadoc for {spring-amqp-java-docs}/core/QueueBuilder.html[`org.springframework.amqp.core.QueueBuilder`] and {spring-amqp-java-docs}/core/ExchangeBuilder.html[`org.springframework.amqp.core.ExchangeBuilder`] for more information. +See the Javadoc for javadoc:org.springframework.amqp.core.QueueBuilder[`org.springframework.amqp.core.QueueBuilder`] and javadoc:org.springframework.amqp.core.ExchangeBuilder[`org.springframework.amqp.core.ExchangeBuilder`] for more information. Starting with version 2.0, the `ExchangeBuilder` now creates durable exchanges by default, to be consistent with the simple constructors on the individual `AbstractExchange` classes. To make a non-durable exchange with the builder, use `.durable(false)` before invoking `.build()`. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc b/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc index c1b278a820..f85b670af9 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/connections.adoc @@ -483,7 +483,7 @@ public class MyService { ---- It is important to unbind the resource after use. -For more information, see the {spring-amqp-java-docs}/rabbit/connection/AbstractRoutingConnectionFactory.html[JavaDoc] for `AbstractRoutingConnectionFactory`. +For more information, see the javadoc:org.springframework.amqp.rabbit.connection.AbstractRoutingConnectionFactory[JavaDoc] for `AbstractRoutingConnectionFactory`. Starting with version 1.4, `RabbitTemplate` supports the SpEL `sendConnectionFactorySelectorExpression` and `receiveConnectionFactorySelectorExpression` properties, which are evaluated on each AMQP protocol interaction operation (`send`, `sendAndReceive`, `receive`, or `receiveAndReply`), resolving to a `lookupKey` value for the provided `AbstractRoutingConnectionFactory`. You can use bean references, such as `@vHostResolver.getVHost(#root)` in the expression. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/listener-queues.adoc b/src/reference/antora/modules/ROOT/pages/amqp/listener-queues.adoc index ed6101ec5c..c9972b3688 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/listener-queues.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/listener-queues.adoc @@ -8,7 +8,7 @@ Container can be initially configured to listen on zero queues. Queues can be added and removed at runtime. The `SimpleMessageListenerContainer` recycles (cancels and re-creates) all consumers when any pre-fetched messages have been processed. The `DirectMessageListenerContainer` creates/cancels individual consumer(s) for each queue without affecting consumers on other queues. -See the {spring-amqp-java-docs}/rabbit/listener/AbstractMessageListenerContainer.html[Javadoc] for the `addQueues`, `addQueueNames`, `removeQueues` and `removeQueueNames` methods. +See the javadoc:org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer[Javadoc] for the `addQueues`, `addQueueNames`, `removeQueues` and `removeQueueNames` methods. If not all queues are available, the container tries to passively declare (and consume from) the missing queues every 60 seconds. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/enable.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/enable.adoc index 422de556ef..c9d37e7a09 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/enable.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/async-annotation-driven/enable.adoc @@ -37,7 +37,7 @@ In this case, and ignoring the RabbitMQ infrastructure setup, the `processOrder` You can customize the listener container factory to use for each annotation, or you can configure an explicit default by implementing the `RabbitListenerConfigurer` interface. The default is required only if at least one endpoint is registered without a specific container factory. -See the {spring-amqp-java-docs}/rabbit/annotation/RabbitListenerConfigurer.html[Javadoc] for full details and examples. +See the javadoc:org.springframework.amqp.rabbit.annotation.RabbitListenerConfigurer[Javadoc] for full details and examples. The container factories provide methods for adding `MessagePostProcessor` instances that are applied after receiving messages (before invoking the listener) and before sending replies. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer.adoc b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer.adoc index bc581083ad..8ccb1548eb 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/receiving-messages/micrometer.adoc @@ -2,7 +2,7 @@ = Micrometer Integration :page-section-summary-toc: 1 -NOTE: This section documents the integration with {micrometer-micrometer-docs}[Micrometer]. +NOTE: This section documents the integration with {micrometer-docs}[Micrometer]. For integration with Micrometer Observation, see xref:amqp/receiving-messages/micrometer-observation.adoc[Micrometer Observation]. Starting with version 2.2, the listener containers will automatically create and update Micrometer `Timer` s for the listener, if `Micrometer` is detected on the class path, and a single `MeterRegistry` is present in the application context (or exactly one is annotated `@Primary`, such as when using Spring Boot). diff --git a/src/reference/antora/modules/ROOT/pages/amqp/request-reply.adoc b/src/reference/antora/modules/ROOT/pages/amqp/request-reply.adoc index 3689437cb3..e3a8634e95 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/request-reply.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/request-reply.adoc @@ -6,11 +6,11 @@ Those methods are quite useful for request-reply scenarios, since they handle th Similar request-reply methods are also available where the `MessageConverter` is applied to both the request and reply. Those methods are named `convertSendAndReceive`. -See the {spring-amqp-java-docs}/core/AmqpTemplate.html[Javadoc of `AmqpTemplate`] for more detail. +See the javadoc:org.springframework.amqp.core.AmqpTemplate[Javadoc of `AmqpTemplate`] for more detail. Starting with version 1.5.0, each of the `sendAndReceive` method variants has an overloaded version that takes `CorrelationData`. Together with a properly configured connection factory, this enables the receipt of publisher confirms for the send side of the operation. -See xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns] and the {spring-amqp-java-docs}/rabbit/core/RabbitOperations.html[Javadoc for `RabbitOperations`] for more information. +See xref:amqp/template.adoc#template-confirms[Correlated Publisher Confirms and Returns] and the javadoc:org.springframework.amqp.rabbit.core.RabbitOperations[Javadoc for `RabbitOperations`] for more information. Starting with version 2.0, there are variants of these methods (`convertSendAndReceiveAsType`) that take an additional `ParameterizedTypeReference` argument to convert complex returned types. The template must be configured with a `SmartMessageConverter`. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/sending-messages.adoc b/src/reference/antora/modules/ROOT/pages/amqp/sending-messages.adoc index 6ff5f1257d..eff9bd900f 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/sending-messages.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/sending-messages.adoc @@ -100,7 +100,7 @@ Message message = MessageBuilder.withBody("foo".getBytes()) .build(); ---- -Each of the properties defined on the {spring-amqp-java-docs}/core/MessageProperties.html[`MessageProperties`] can be set. +Each of the properties defined on the javadoc:org.springframework.amqp.core.MessageProperties[] can be set. Other methods include `setHeader(String key, String value)`, `removeHeader(String key)`, `removeHeaders()`, and `copyProperties(MessageProperties properties)`. Each property setting method has a `set*IfAbsent()` variant. In the cases where a default initial value exists, the method is named `set*IfAbsentOrDefault()`. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/transactions.adoc b/src/reference/antora/modules/ROOT/pages/amqp/transactions.adoc index 59bb842f7b..796be4f4db 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/transactions.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/transactions.adoc @@ -116,13 +116,13 @@ See xref:amqp/containerAttributes.adoc[Message Listener Container Configuration] [[using-rabbittransactionmanager]] == Using `RabbitTransactionManager` -The {spring-amqp-java-docs}/rabbit/transaction/RabbitTransactionManager.html[RabbitTransactionManager] is an alternative to executing Rabbit operations within, and synchronized with, external transactions. -This transaction manager is an implementation of the {spring-framework-java-docs}/transaction/PlatformTransactionManager.html[`PlatformTransactionManager`] interface and should be used with a single Rabbit `ConnectionFactory`. +The javadoc:org.springframework.amqp.rabbit.transaction.RabbitTransactionManager[] is an alternative to executing Rabbit operations within, and synchronized with, external transactions. +This transaction manager is an implementation of the javadoc:org.springframework.transaction.PlatformTransactionManager[] interface and should be used with a single Rabbit `ConnectionFactory`. IMPORTANT: This strategy is not able to provide XA transactions -- for example, in order to share transactions between messaging and database access. Application code is required to retrieve the transactional Rabbit resources through `ConnectionFactoryUtils.getTransactionalResourceHolder(ConnectionFactory, boolean)` instead of a standard `Connection.createChannel()` call with subsequent channel creation. -When using Spring AMQP's {spring-amqp-java-docs}/rabbit/core/RabbitTemplate.html[RabbitTemplate], it will autodetect a thread-bound Channel and automatically participate in its transaction. +When using Spring AMQP's javadoc:org.springframework.amqp.rabbit.core.RabbitTemplate[], it will autodetect a thread-bound Channel and automatically participate in its transaction. With Java Configuration, you can setup a new RabbitTransactionManager by using the following bean: diff --git a/src/reference/antora/modules/ROOT/pages/sample-apps.adoc b/src/reference/antora/modules/ROOT/pages/sample-apps.adoc index 90a2775e53..842c5c72a2 100644 --- a/src/reference/antora/modules/ROOT/pages/sample-apps.adoc +++ b/src/reference/antora/modules/ROOT/pages/sample-apps.adoc @@ -351,4 +351,4 @@ Spring applications, when sending JSON, set the `__TypeId__` header to the fully The `spring-rabbit-json` sample explores several techniques to convert the JSON from a non-Spring application. -See also xref:amqp/message-converters.adoc#json-message-converter[`Jackson2JsonMessageConverter`] as well as the {spring-amqp-java-docs}/index.html?org/springframework/amqp/support/converter/DefaultClassMapper.html[Javadoc for the `DefaultClassMapper`]. +See also xref:amqp/message-converters.adoc#json-message-converter[`Jackson2JsonMessageConverter`] as well as the javadoc:org.springframework.amqp.support.converter.DefaultClassMapper[Javadoc for the `DefaultClassMapper`]. From f951ea5bb67b6f826a906dd6a13268726a182dbb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 22 Feb 2025 02:37:59 +0000 Subject: [PATCH 691/737] Bump io.micrometer:micrometer-bom from 1.15.0-M2 to 1.15.0-SNAPSHOT (#2986) Bumps [io.micrometer:micrometer-bom](https://github.com/micrometer-metrics/micrometer) from 1.15.0-M2 to 1.15.0-SNAPSHOT. - [Release notes](https://github.com/micrometer-metrics/micrometer/releases) - [Commits](https://github.com/micrometer-metrics/micrometer/commits) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 15d7df7848..2c0eaf0877 100644 --- a/build.gradle +++ b/build.gradle @@ -56,7 +56,7 @@ ext { log4jVersion = '2.24.3' logbackVersion = '1.5.16' micrometerDocsVersion = '1.0.4' - micrometerVersion = '1.15.0-M2' + micrometerVersion = '1.15.0-SNAPSHOT' micrometerTracingVersion = '1.5.0-M2' mockitoVersion = '5.15.2' rabbitmqStreamVersion = '0.22.0' From 6504e76f046f8924dc996da785d6579e858f54a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 22 Feb 2025 02:39:19 +0000 Subject: [PATCH 692/737] Bump org.springframework.data:spring-data-bom (#2987) Bumps [org.springframework.data:spring-data-bom](https://github.com/spring-projects/spring-data-bom) from 2025.1.0-M1 to 2025.1.0-SNAPSHOT. - [Release notes](https://github.com/spring-projects/spring-data-bom/releases) - [Commits](https://github.com/spring-projects/spring-data-bom/commits) --- updated-dependencies: - dependency-name: org.springframework.data:spring-data-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2c0eaf0877..cb3bbb7600 100644 --- a/build.gradle +++ b/build.gradle @@ -62,7 +62,7 @@ ext { rabbitmqStreamVersion = '0.22.0' rabbitmqVersion = '5.24.0' reactorVersion = '2024.0.3' - springDataVersion = '2025.1.0-M1' + springDataVersion = '2025.1.0-SNAPSHOT' springRetryVersion = '2.0.11' springVersion = '7.0.0-M2' testcontainersVersion = '1.20.4' From 6685f1656145e029d7b84d30ac2ad1e19fd8f439 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 22 Feb 2025 02:39:39 +0000 Subject: [PATCH 693/737] Bump org.springframework:spring-framework-bom (#2988) Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 7.0.0-M2 to 7.0.0-SNAPSHOT. - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/commits) --- updated-dependencies: - dependency-name: org.springframework:spring-framework-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index cb3bbb7600..740bb41fe9 100644 --- a/build.gradle +++ b/build.gradle @@ -64,7 +64,7 @@ ext { reactorVersion = '2024.0.3' springDataVersion = '2025.1.0-SNAPSHOT' springRetryVersion = '2.0.11' - springVersion = '7.0.0-M2' + springVersion = '7.0.0-SNAPSHOT' testcontainersVersion = '1.20.4' javaProjects = subprojects - project(':spring-amqp-bom') From f5c64ee14bc23936e0eeb18b095fb3951738e63c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 22 Feb 2025 02:41:55 +0000 Subject: [PATCH 694/737] Bump io.micrometer:micrometer-tracing-bom (#2989) Bumps [io.micrometer:micrometer-tracing-bom](https://github.com/micrometer-metrics/tracing) from 1.5.0-M2 to 1.5.0-SNAPSHOT. - [Release notes](https://github.com/micrometer-metrics/tracing/releases) - [Commits](https://github.com/micrometer-metrics/tracing/commits) --- updated-dependencies: - dependency-name: io.micrometer:micrometer-tracing-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 740bb41fe9..2f86691323 100644 --- a/build.gradle +++ b/build.gradle @@ -57,7 +57,7 @@ ext { logbackVersion = '1.5.16' micrometerDocsVersion = '1.0.4' micrometerVersion = '1.15.0-SNAPSHOT' - micrometerTracingVersion = '1.5.0-M2' + micrometerTracingVersion = '1.5.0-SNAPSHOT' mockitoVersion = '5.15.2' rabbitmqStreamVersion = '0.22.0' rabbitmqVersion = '5.24.0' From fbd93a9dfb588e530cb339229e312c7263b5d971 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 08:56:49 -0500 Subject: [PATCH 695/737] Bump org.testcontainers:testcontainers-bom from 1.20.4 to 1.20.5 Bumps [org.testcontainers:testcontainers-bom](https://github.com/testcontainers/testcontainers-java) from 1.20.4 to 1.20.5. - [Release notes](https://github.com/testcontainers/testcontainers-java/releases) - [Changelog](https://github.com/testcontainers/testcontainers-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/testcontainers/testcontainers-java/compare/1.20.4...1.20.5) --- updated-dependencies: - dependency-name: org.testcontainers:testcontainers-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2f86691323..35e67de831 100644 --- a/build.gradle +++ b/build.gradle @@ -65,7 +65,7 @@ ext { springDataVersion = '2025.1.0-SNAPSHOT' springRetryVersion = '2.0.11' springVersion = '7.0.0-SNAPSHOT' - testcontainersVersion = '1.20.4' + testcontainersVersion = '1.20.5' javaProjects = subprojects - project(':spring-amqp-bom') } From f486a30e636f68f1ea08f0b562f7787ef21e3452 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 26 Feb 2025 13:13:15 -0500 Subject: [PATCH 696/737] Introduce `spring-rabbitmq-client` for RabbitMQ AMQP 1.0 Fixes: https://github.com/spring-projects/spring-amqp/issues/2991 Fixes: https://github.com/spring-projects/spring-amqp/issues/2992 Fixes: https://github.com/spring-projects/spring-amqp/issues/2993 * Add `QueueInformation.type` since RabbitMQ AMQP Client exposes such an info --- build.gradle | 23 + .../amqp/core/QueueInformation.java | 36 +- .../springframework/amqp/utils/JavaUtils.java | 4 +- .../amqp/rabbit/core/RabbitAdmin.java | 2 +- .../amqp/rabbit/core/RabbitAdminTests.java | 2 +- .../client/AmqpConnectionFactoryBean.java | 286 +++++++++ .../amqp/rabbitmq/client/RabbitAmqpAdmin.java | 570 ++++++++++++++++++ .../rabbitmq/client/RabbitAmqpTemplate.java | 494 +++++++++++++++ .../amqp/rabbitmq/client/package-info.java | 5 + .../rabbitmq/client/RabbitAmqpAdminTests.java | 145 +++++ .../client/RabbitAmqpTemplateTests.java | 107 ++++ .../rabbitmq/client/RabbitAmqpTestBase.java | 69 +++ .../src/test/resources/log4j2-test.xml | 14 + 13 files changed, 1745 insertions(+), 12 deletions(-) create mode 100644 spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/AmqpConnectionFactoryBean.java create mode 100644 spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdmin.java create mode 100644 spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplate.java create mode 100644 spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/package-info.java create mode 100644 spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdminTests.java create mode 100644 spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplateTests.java create mode 100644 spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTestBase.java create mode 100644 spring-rabbitmq-client/src/test/resources/log4j2-test.xml diff --git a/build.gradle b/build.gradle index 35e67de831..f226c681b4 100644 --- a/build.gradle +++ b/build.gradle @@ -59,6 +59,7 @@ ext { micrometerVersion = '1.15.0-SNAPSHOT' micrometerTracingVersion = '1.5.0-SNAPSHOT' mockitoVersion = '5.15.2' + rabbitmqAmqpClientVersion = '0.4.0' rabbitmqStreamVersion = '0.22.0' rabbitmqVersion = '5.24.0' reactorVersion = '2024.0.3' @@ -472,6 +473,28 @@ project('spring-rabbit-stream') { } } +project('spring-rabbitmq-client') { + description = 'Spring RabbitMQ Client for AMQP 1.0' + + dependencies { + api project(':spring-rabbit') + api "com.rabbitmq.client:amqp-client:$rabbitmqAmqpClientVersion" + api 'io.micrometer:micrometer-observation' + + testApi project(':spring-rabbit-junit') + + testRuntimeOnly 'com.fasterxml.jackson.core:jackson-databind' + + testImplementation 'org.testcontainers:rabbitmq' + testImplementation 'org.testcontainers:junit-jupiter' + testImplementation 'org.apache.logging.log4j:log4j-slf4j-impl' + testImplementation 'io.micrometer:micrometer-observation-test' + testImplementation 'io.micrometer:micrometer-tracing-bridge-brave' + testImplementation 'io.micrometer:micrometer-tracing-test' + testImplementation 'io.micrometer:micrometer-tracing-integration-test' + } +} + project('spring-rabbit-junit') { description = 'Spring Rabbit JUnit Support' diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/QueueInformation.java b/spring-amqp/src/main/java/org/springframework/amqp/core/QueueInformation.java index f2d8df2453..2323774978 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/QueueInformation.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/QueueInformation.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 the original author or authors. + * Copyright 2019-2025 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. @@ -21,6 +21,8 @@ * * @author Gary Russell * @author Ngoc Nhan + * @author Artem Bilan + * * @since 2.2 * */ @@ -28,11 +30,13 @@ public class QueueInformation { private final String name; - private final int messageCount; + private final long messageCount; private final int consumerCount; - public QueueInformation(String name, int messageCount, int consumerCount) { + private String type = "classic"; + + public QueueInformation(String name, long messageCount, int consumerCount) { this.name = name; this.messageCount = messageCount; this.consumerCount = consumerCount; @@ -42,7 +46,7 @@ public String getName() { return this.name; } - public int getMessageCount() { + public long getMessageCount() { return this.messageCount; } @@ -50,11 +54,30 @@ public int getConsumerCount() { return this.consumerCount; } + /** + * Return a queue type. + * {@code classic} by default since AMQP 0.9.1 protocol does not return this info in {@code DeclareOk} reply. + * @return a queue type + * @since 4.0 + */ + public String getType() { + return this.type; + } + + /** + * Set a queue type. + * @param type the queue type: {@code quorum}, {@code classic} or {@code stream} + * @since 4.0 + */ + public void setType(String type) { + this.type = type; + } + @Override public int hashCode() { final int prime = 31; int result = 1; - result = prime * result + ((this.name == null) ? 0 : this.name.hashCode()); + result = prime * result + this.name.hashCode(); return result; } @@ -70,9 +93,6 @@ public boolean equals(Object obj) { return false; } QueueInformation other = (QueueInformation) obj; - if (this.name == null) { - return other.name == null; - } return this.name.equals(other.name); } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/utils/JavaUtils.java b/spring-amqp/src/main/java/org/springframework/amqp/utils/JavaUtils.java index edd108e540..ee15135324 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/utils/JavaUtils.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/utils/JavaUtils.java @@ -44,9 +44,9 @@ private JavaUtils() { } /** - * Invoke {@link Consumer#accept(Object)} with the value if the condition is true. + * Invoke {@link Consumer#accept(Object)} with the value if it is not null and the condition is true. * @param condition the condition. - * @param value the value. + * @param value the value. Skipped if null. * @param consumer the consumer. * @param the value type. * @return this. diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java index d04a61f254..1831399049 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitAdmin.java @@ -119,7 +119,7 @@ public class RabbitAdmin implements AmqpAdmin, ApplicationContextAware, Applicat */ public static final Object QUEUE_CONSUMER_COUNT = "QUEUE_CONSUMER_COUNT"; - private static final String DELAYED_MESSAGE_EXCHANGE = "x-delayed-message"; + public static final String DELAYED_MESSAGE_EXCHANGE = "x-delayed-message"; /** Logger available to subclasses. */ protected final Log logger = LogFactory.getLog(getClass()); // NOSONAR diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java index 07465a9d10..2cacb164ff 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/core/RabbitAdminTests.java @@ -163,7 +163,7 @@ public void testProperties() throws Exception { } } - private int messageCount(RabbitAdmin rabbitAdmin, String queueName) { + private long messageCount(RabbitAdmin rabbitAdmin, String queueName) { QueueInformation info = rabbitAdmin.getQueueInfo(queueName); assertThat(info).isNotNull(); return info.getMessageCount(); diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/AmqpConnectionFactoryBean.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/AmqpConnectionFactoryBean.java new file mode 100644 index 0000000000..b0769f5e67 --- /dev/null +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/AmqpConnectionFactoryBean.java @@ -0,0 +1,286 @@ +/* + * Copyright 2025 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.rabbitmq.client; + +import java.time.Duration; +import java.util.function.Consumer; + +import javax.net.ssl.SSLContext; + +import com.rabbitmq.client.amqp.AddressSelector; +import com.rabbitmq.client.amqp.BackOffDelayPolicy; +import com.rabbitmq.client.amqp.Connection; +import com.rabbitmq.client.amqp.ConnectionBuilder; +import com.rabbitmq.client.amqp.ConnectionSettings; +import com.rabbitmq.client.amqp.CredentialsProvider; +import com.rabbitmq.client.amqp.Environment; +import com.rabbitmq.client.amqp.OAuth2Settings; +import com.rabbitmq.client.amqp.Resource; +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.factory.config.AbstractFactoryBean; + +/** + * The {@link AbstractFactoryBean} for RabbitMQ AMQP 1.0 {@link Connection}. + * A Spring-friendly wrapper around {@link Environment#connectionBuilder()}; + * + * @author Artem Bilan + * + * @since 4.0 + */ +public class AmqpConnectionFactoryBean extends AbstractFactoryBean { + + private final ConnectionBuilder connectionBuilder; + + public AmqpConnectionFactoryBean(Environment amqpEnvironment) { + this.connectionBuilder = amqpEnvironment.connectionBuilder(); + } + + public AmqpConnectionFactoryBean setHost(String host) { + this.connectionBuilder.host(host); + return this; + } + + public AmqpConnectionFactoryBean setPort(int port) { + this.connectionBuilder.port(port); + return this; + } + + public AmqpConnectionFactoryBean setUsername(String username) { + this.connectionBuilder.username(username); + return this; + } + + public AmqpConnectionFactoryBean setPassword(String password) { + this.connectionBuilder.password(password); + return this; + } + + public AmqpConnectionFactoryBean setVirtualHost(String virtualHost) { + this.connectionBuilder.virtualHost(virtualHost); + return this; + } + + public AmqpConnectionFactoryBean setUri(String uri) { + this.connectionBuilder.uri(uri); + return this; + } + + public AmqpConnectionFactoryBean setUris(String... uris) { + this.connectionBuilder.uris(uris); + return this; + } + + public AmqpConnectionFactoryBean setIdleTimeout(Duration idleTimeout) { + this.connectionBuilder.idleTimeout(idleTimeout); + return this; + } + + public AmqpConnectionFactoryBean setAddressSelector(AddressSelector addressSelector) { + this.connectionBuilder.addressSelector(addressSelector); + return this; + } + + public AmqpConnectionFactoryBean setCredentialsProvider(CredentialsProvider credentialsProvider) { + this.connectionBuilder.credentialsProvider(credentialsProvider); + return this; + } + + public AmqpConnectionFactoryBean setSaslMechanism(SaslMechanism saslMechanism) { + this.connectionBuilder.saslMechanism(saslMechanism.name()); + return this; + } + + public AmqpConnectionFactoryBean setTls(Consumer tlsCustomizer) { + tlsCustomizer.accept(new Tls(this.connectionBuilder.tls())); + return this; + } + + public AmqpConnectionFactoryBean setAffinity(Consumer affinityCustomizer) { + affinityCustomizer.accept(new Affinity(this.connectionBuilder.affinity())); + return this; + } + + public AmqpConnectionFactoryBean setOAuth2(Consumer oauth2Customizer) { + oauth2Customizer.accept(new OAuth2(this.connectionBuilder.oauth2())); + return this; + } + + public AmqpConnectionFactoryBean setRecovery(Consumer recoveryCustomizer) { + recoveryCustomizer.accept(new Recovery(this.connectionBuilder.recovery())); + return this; + } + + public AmqpConnectionFactoryBean setListeners(Resource.StateListener... listeners) { + this.connectionBuilder.listeners(listeners); + return this; + } + + @Override + public @Nullable Class getObjectType() { + return Connection.class; + } + + @Override + protected Connection createInstance() { + return this.connectionBuilder.build(); + } + + @Override + protected void destroyInstance(@Nullable Connection instance) { + if (instance != null) { + instance.close(); + } + } + + public enum SaslMechanism { + + PLAIN, ANONYMOUS, EXTERNAL + + } + + public static final class Tls { + + private final ConnectionSettings.TlsSettings tls; + + private Tls(ConnectionSettings.TlsSettings tls) { + this.tls = tls; + } + + public Tls hostnameVerification() { + this.tls.hostnameVerification(); + return this; + } + + public Tls hostnameVerification(boolean hostnameVerification) { + this.tls.hostnameVerification(hostnameVerification); + return this; + } + + public Tls sslContext(SSLContext sslContext) { + this.tls.sslContext(sslContext); + return this; + } + + public Tls trustEverything() { + this.tls.trustEverything(); + return this; + } + + } + + public static final class Affinity { + + private final ConnectionSettings.Affinity affinity; + + private Affinity(ConnectionSettings.Affinity affinity) { + this.affinity = affinity; + } + + public Affinity queue(String queue) { + this.affinity.queue(queue); + return this; + } + + public Affinity operation(ConnectionSettings.Affinity.Operation operation) { + this.affinity.operation(operation); + return this; + } + + public Affinity reuse(boolean reuse) { + this.affinity.reuse(reuse); + return this; + } + + public Affinity strategy(ConnectionSettings.AffinityStrategy strategy) { + this.affinity.strategy(strategy); + return this; + } + + } + + public static final class OAuth2 { + + private final OAuth2Settings oAuth2Settings; + + private OAuth2(OAuth2Settings oAuth2Settings) { + this.oAuth2Settings = oAuth2Settings; + } + + public OAuth2 tokenEndpointUri(String uri) { + this.oAuth2Settings.tokenEndpointUri(uri); + return this; + } + + public OAuth2 clientId(String clientId) { + this.oAuth2Settings.clientId(clientId); + return this; + } + + public OAuth2 clientSecret(String clientSecret) { + this.oAuth2Settings.clientSecret(clientSecret); + return this; + } + + public OAuth2 grantType(String grantType) { + this.oAuth2Settings.grantType(grantType); + return this; + } + + public OAuth2 parameter(String name, String value) { + this.oAuth2Settings.parameter(name, value); + return this; + } + + public OAuth2 shared(boolean shared) { + this.oAuth2Settings.shared(shared); + return this; + } + + public OAuth2 sslContext(SSLContext sslContext) { + this.oAuth2Settings.tls().sslContext(sslContext); + return this; + } + + } + + public static final class Recovery { + + private final ConnectionBuilder.RecoveryConfiguration recoveryConfiguration; + + private Recovery(ConnectionBuilder.RecoveryConfiguration recoveryConfiguration) { + this.recoveryConfiguration = recoveryConfiguration; + } + + public Recovery activated(boolean activated) { + this.recoveryConfiguration.activated(activated); + return this; + } + + public Recovery backOffDelayPolicy(BackOffDelayPolicy backOffDelayPolicy) { + this.recoveryConfiguration.backOffDelayPolicy(backOffDelayPolicy); + return this; + } + + public Recovery topology(boolean activated) { + this.recoveryConfiguration.topology(activated); + return this; + } + + } + +} diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdmin.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdmin.java new file mode 100644 index 0000000000..226a3413a7 --- /dev/null +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdmin.java @@ -0,0 +1,570 @@ +/* + * Copyright 2025 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.rabbitmq.client; + +import java.io.IOException; +import java.util.Collection; +import java.util.LinkedList; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicReference; + +import com.rabbitmq.client.amqp.AmqpException; +import com.rabbitmq.client.amqp.Connection; +import com.rabbitmq.client.amqp.Management; +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.core.AmqpAdmin; +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.Declarable; +import org.springframework.amqp.core.DeclarableCustomizer; +import org.springframework.amqp.core.Declarables; +import org.springframework.amqp.core.Exchange; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.QueueInformation; +import org.springframework.amqp.rabbit.core.DeclarationExceptionEvent; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.context.SmartLifecycle; +import org.springframework.core.log.LogAccessor; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.core.task.TaskExecutor; +import org.springframework.jmx.export.annotation.ManagedOperation; +import org.springframework.jmx.export.annotation.ManagedResource; +import org.springframework.util.Assert; + +/** + * The {@link AmqpAdmin} implementation for RabbitMQ AMQP 1.0 client. + * + * @author Artem Bilan + * + * @since 4.0 + */ +@ManagedResource(description = "Admin Tasks") +public class RabbitAmqpAdmin + implements AmqpAdmin, ApplicationContextAware, ApplicationEventPublisherAware, BeanNameAware, SmartLifecycle { + + private static final LogAccessor LOG = new LogAccessor(RabbitAmqpAdmin.class); + + public static final String QUEUE_TYPE = "QUEUE_TYPE"; + + private final Connection amqpConnection; + + private TaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(); + + private boolean ignoreDeclarationExceptions; + + private @Nullable ApplicationContext applicationContext; + + private @Nullable ApplicationEventPublisher applicationEventPublisher; + + @SuppressWarnings("NullAway.Init") + private String beanName; + + private boolean explicitDeclarationsOnly; + + private boolean autoStartup = true; + + private volatile @Nullable DeclarationExceptionEvent lastDeclarationExceptionEvent; + + private volatile boolean running = false; + + public RabbitAmqpAdmin(Connection amqpConnection) { + this.amqpConnection = amqpConnection; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } + + @Override + public void setBeanName(String name) { + this.beanName = name; + } + + public void setIgnoreDeclarationExceptions(boolean ignoreDeclarationExceptions) { + this.ignoreDeclarationExceptions = ignoreDeclarationExceptions; + } + + /** + * Set a task executor to use for async operations. Currently only used + * with {@link #purgeQueue(String, boolean)}. + * @param taskExecutor the executor to use. + */ + public void setTaskExecutor(TaskExecutor taskExecutor) { + Assert.notNull(taskExecutor, "'taskExecutor' cannot be null"); + this.taskExecutor = taskExecutor; + } + + /** + * Set to true to only declare {@link Declarable} beans that are explicitly configured + * to be declared by this admin. + * @param explicitDeclarationsOnly true to ignore beans with no admin declaration + * configuration. + */ + public void setExplicitDeclarationsOnly(boolean explicitDeclarationsOnly) { + this.explicitDeclarationsOnly = explicitDeclarationsOnly; + } + + /** + * @return the last {@link DeclarationExceptionEvent} that was detected in this admin. + */ + public @Nullable DeclarationExceptionEvent getLastDeclarationExceptionEvent() { + return this.lastDeclarationExceptionEvent; + } + + @Override + public boolean isAutoStartup() { + return this.autoStartup; + } + + public void setAutoStartup(boolean autoStartup) { + this.autoStartup = autoStartup; + } + + @Override + public int getPhase() { + return Integer.MIN_VALUE; + } + + @Override + public void start() { + if (!this.running) { + initialize(); + this.running = true; + } + } + + @Override + public void stop() { + this.running = false; + } + + @Override + public boolean isRunning() { + return this.running; + } + + /** + * Declares all the exchanges, queues and bindings in the enclosing application context, if any. It should be safe + * (but unnecessary) to call this method more than once. + */ + @Override + public void initialize() { + redeclareBeanDeclarables(); + } + + /** + * Process bean declarables. + */ + private void redeclareBeanDeclarables() { + if (this.applicationContext == null) { + LOG.debug("no ApplicationContext has been set, cannot auto-declare Exchanges, Queues, and Bindings"); + return; + } + + LOG.debug("Initializing declarations"); + Collection contextExchanges = new LinkedList<>( + this.applicationContext.getBeansOfType(Exchange.class).values()); + Collection contextQueues = new LinkedList<>( + this.applicationContext.getBeansOfType(Queue.class).values()); + Collection contextBindings = new LinkedList<>( + this.applicationContext.getBeansOfType(Binding.class).values()); + Collection customizers = + this.applicationContext.getBeansOfType(DeclarableCustomizer.class).values(); + + processDeclarables(contextExchanges, contextQueues, contextBindings, + this.applicationContext.getBeansOfType(Declarables.class, false, true).values()); + + final Collection exchanges = filterDeclarables(contextExchanges, customizers); + final Collection queues = filterDeclarables(contextQueues, customizers); + final Collection bindings = filterDeclarables(contextBindings, customizers); + + for (Exchange exchange : exchanges) { + if ((!exchange.isDurable() || exchange.isAutoDelete())) { + LOG.info(() -> "Auto-declaring a non-durable or auto-delete Exchange (" + + exchange.getName() + + ") durable:" + exchange.isDurable() + ", auto-delete:" + exchange.isAutoDelete() + ". " + + "It will be deleted by the broker if it shuts down, and can be redeclared by closing and " + + "reopening the connection."); + } + } + + for (Queue queue : queues) { + if ((!queue.isDurable() || queue.isAutoDelete() || queue.isExclusive())) { + LOG.info(() -> "Auto-declaring a non-durable, auto-delete, or exclusive Queue (" + + queue.getName() + + ") durable:" + queue.isDurable() + ", auto-delete:" + queue.isAutoDelete() + ", exclusive:" + + queue.isExclusive() + ". " + + "It will be redeclared if the broker stops and is restarted while the connection factory is " + + "alive, but all messages will be lost."); + } + } + + if (exchanges.isEmpty() && queues.isEmpty() && bindings.isEmpty()) { + LOG.debug("Nothing to declare"); + return; + } + + try (Management management = this.amqpConnection.management()) { + exchanges.forEach((exchange) -> doDeclareExchange(management, exchange)); + queues.forEach((queue) -> doDeclareQueue(management, queue)); + bindings.forEach((binding) -> doDeclareBinding(management, binding)); + } + + LOG.debug("Declarations finished"); + } + + /** + * Remove any instances that should not be declared by this admin. + * @param declarables the collection of {@link Declarable}s. + * @param customizers a collection if {@link DeclarableCustomizer} beans. + * @param the declarable type. + * @return a new collection containing {@link Declarable}s that should be declared by this + * admin. + */ + @SuppressWarnings({"unchecked", "NullAway"}) // Dataflow analysis limitation + private Collection filterDeclarables(Collection declarables, + Collection customizers) { + + return declarables.stream() + .filter(dec -> dec.shouldDeclare() && declarableByMe(dec)) + .map(dec -> { + if (customizers.isEmpty()) { + return dec; + } + AtomicReference ref = new AtomicReference<>(dec); + customizers.forEach(cust -> ref.set((T) cust.apply(ref.get()))); + return ref.get(); + }) + .toList(); + } + + private boolean declarableByMe(T dec) { + return (dec.getDeclaringAdmins().isEmpty() && !this.explicitDeclarationsOnly) // NOSONAR boolean complexity + || dec.getDeclaringAdmins().contains(this) + || dec.getDeclaringAdmins().contains(this.beanName); + } + + @Override + public void declareExchange(Exchange exchange) { + try (Management management = this.amqpConnection.management()) { + doDeclareExchange(management, exchange); + } + } + + private void doDeclareExchange(Management management, Exchange exchange) { + Management.ExchangeSpecification exchangeSpecification = + management.exchange(exchange.getName()) + .type(exchange.isDelayed() ? RabbitAdmin.DELAYED_MESSAGE_EXCHANGE : exchange.getType()) +// .durable(exchange.isDurable()) +// .internal(exchange.isInternal()) + .autoDelete(exchange.isAutoDelete()); + Map arguments = exchange.getArguments(); + if (arguments != null) { + arguments.forEach(exchangeSpecification::argument); + } + if (exchange.isDelayed()) { + exchangeSpecification.argument("x-delayed-type", exchange.getType()); + } + try { + exchangeSpecification.declare(); + } + catch (AmqpException ex) { + logOrRethrowDeclarationException(exchange, "exchange", ex); + } + } + + @Override + @ManagedOperation(description = "Delete an exchange from the broker") + public boolean deleteExchange(String exchangeName) { + if (isDeletingDefaultExchange(exchangeName)) { + return false; + } + + try (Management management = this.amqpConnection.management()) { + management.exchangeDelete(exchangeName); + } + return true; + } + + @Override + public @Nullable Queue declareQueue() { + try (Management management = this.amqpConnection.management()) { + return doDeclareQueue(management); + } + } + + private @Nullable Queue doDeclareQueue(Management management) { + try { + Management.QueueInfo queueInfo = + management.queue() + .autoDelete(true) + .exclusive(true) + .classic() + // .durable(false) + .queue() + .declare(); + return new Queue(queueInfo.name(), false, true, true); + } + catch (AmqpException ex) { + logOrRethrowDeclarationException(null, "queue", ex); + } + return null; + } + + @Override + public @Nullable String declareQueue(Queue queue) { + try (Management management = this.amqpConnection.management()) { + return doDeclareQueue(management, queue); + } + } + + private @Nullable String doDeclareQueue(Management management, Queue queue) { + Management.QueueSpecification queueSpecification = + management.queue(queue.getName()) + .autoDelete(queue.isAutoDelete()) + .exclusive(queue.isExclusive()) + .classic() +// .durable(queue.isDurable()) + .queue(); + + queue.getArguments().forEach(queueSpecification::argument); + + try { + String actualName = queueSpecification.declare().name(); + queue.setActualName(actualName); + return actualName; + } + catch (AmqpException ex) { + logOrRethrowDeclarationException(queue, "queue", ex); + } + return null; + } + + @Override + @ManagedOperation(description = "Delete a queue from the broker") + public boolean deleteQueue(String queueName) { + deleteQueue(queueName, false, false); + return true; + } + + @Override + @ManagedOperation(description = + "Delete a queue from the broker if unused and empty (when corresponding arguments are true") + public void deleteQueue(String queueName, boolean unused, boolean empty) { + try (Management management = this.amqpConnection.management()) { + Management.QueueInfo queueInfo = management.queueInfo(queueName); + if ((!unused || queueInfo.consumerCount() == 0) + && (!empty || queueInfo.messageCount() == 0)) { + + management.queueDelete(queueName); + } + } + } + + @Override + @ManagedOperation(description = "Purge a queue and optionally don't wait for the purge to occur") + public void purgeQueue(String queueName, boolean noWait) { + if (noWait) { + this.taskExecutor.execute(() -> purgeQueue(queueName)); + } + else { + purgeQueue(queueName); + } + } + + @Override + @ManagedOperation(description = "Purge a queue and return the number of messages purged") + public int purgeQueue(String queueName) { + try (Management management = this.amqpConnection.management()) { + management.queuePurge(queueName); + } + return 0; + } + + @Override + public void declareBinding(Binding binding) { + try (Management management = this.amqpConnection.management()) { + doDeclareBinding(management, binding); + } + } + + private void doDeclareBinding(Management management, Binding binding) { + try { + Management.BindingSpecification bindingSpecification = + management.binding() + .sourceExchange(binding.getExchange()) + .key(binding.getRoutingKey()) + .arguments(binding.getArguments()); + if (binding.isDestinationQueue()) { + bindingSpecification.destinationQueue(binding.getDestination()); + } + else { + bindingSpecification.destinationExchange(binding.getDestination()); + } + bindingSpecification.bind(); + } + catch (AmqpException ex) { + logOrRethrowDeclarationException(binding, "binding", ex); + } + } + + @Override + public void removeBinding(Binding binding) { + if (binding.isDestinationQueue() && isRemovingImplicitQueueBinding(binding)) { + return; + } + + try (Management management = this.amqpConnection.management()) { + Management.UnbindSpecification unbindSpecification = + management.unbind() + .sourceExchange(binding.getExchange()) + .key(binding.getRoutingKey()) + .arguments(binding.getArguments()); + + if (binding.isDestinationQueue()) { + unbindSpecification.destinationQueue(binding.getDestination()); + } + else { + unbindSpecification.destinationExchange(binding.getDestination()); + } + unbindSpecification.unbind(); + } + } + + /** + * Returns 4 properties {@link RabbitAdmin#QUEUE_NAME}, {@link RabbitAdmin#QUEUE_MESSAGE_COUNT}, + * {@link RabbitAdmin#QUEUE_CONSUMER_COUNT}, {@link #QUEUE_TYPE}, or null if the queue doesn't exist. + */ + @Override + @ManagedOperation(description = "Get queue name, message count and consumer count") + public @Nullable Properties getQueueProperties(final String queueName) { + QueueInformation queueInfo = getQueueInfo(queueName); + if (queueInfo != null) { + Properties props = new Properties(); + props.put(RabbitAdmin.QUEUE_NAME, queueInfo.getName()); + props.put(RabbitAdmin.QUEUE_MESSAGE_COUNT, queueInfo.getMessageCount()); + props.put(RabbitAdmin.QUEUE_CONSUMER_COUNT, queueInfo.getConsumerCount()); + props.put(QUEUE_TYPE, queueInfo.getType()); + return props; + } + else { + return null; + } + } + + @Override + public @Nullable QueueInformation getQueueInfo(String queueName) { + try (Management management = this.amqpConnection.management()) { + Management.QueueInfo queueInfo = management.queueInfo(queueName); + QueueInformation queueInformation = + new QueueInformation(queueInfo.name(), queueInfo.messageCount(), queueInfo.consumerCount()); + queueInformation.setType(queueInfo.type().name().toLowerCase()); + return queueInformation; + } + } + + private void logOrRethrowDeclarationException(@Nullable Declarable element, + String elementType, T t) throws T { + + publishDeclarationExceptionEvent(element, t); + if (this.ignoreDeclarationExceptions || (element != null && element.isIgnoreDeclarationExceptions())) { + if (LOG.isDebugEnabled()) { + LOG.debug(t, "Failed to declare " + elementType + + ": " + (element == null ? "broker-generated" : element) + + ", continuing..."); + } + else if (LOG.isWarnEnabled()) { + Throwable cause = t; + if (t instanceof IOException && t.getCause() != null) { + cause = t.getCause(); + } + LOG.warn("Failed to declare " + elementType + + ": " + (element == null ? "broker-generated" : element) + + ", continuing... " + cause); + } + } + else { + throw t; + } + } + + private void publishDeclarationExceptionEvent(@Nullable Declarable element, Throwable ex) { + DeclarationExceptionEvent event = new DeclarationExceptionEvent(this, element, ex); + this.lastDeclarationExceptionEvent = event; + if (this.applicationEventPublisher != null) { + this.applicationEventPublisher.publishEvent(event); + } + } + + private static boolean isDeletingDefaultExchange(String exchangeName) { + if (isDefaultExchange(exchangeName)) { + LOG.warn("Default exchange cannot be deleted."); + return true; + } + return false; + } + + private static boolean isDefaultExchange(@Nullable String exchangeName) { + return exchangeName == null || RabbitAdmin.DEFAULT_EXCHANGE_NAME.equals(exchangeName); + } + + private static boolean isRemovingImplicitQueueBinding(Binding binding) { + if (isImplicitQueueBinding(binding)) { + LOG.warn("Cannot remove implicit default exchange binding to queue."); + return true; + } + return false; + } + + private static boolean isImplicitQueueBinding(Binding binding) { + return isDefaultExchange(binding.getExchange()) && + Objects.equals(binding.getDestination(), binding.getRoutingKey()); + } + + private static void processDeclarables(Collection contextExchanges, Collection contextQueues, + Collection contextBindings, Collection declarables) { + + declarables.forEach(d -> { + d.getDeclarables().forEach(declarable -> { + if (declarable instanceof Exchange exch) { + contextExchanges.add(exch); + } + else if (declarable instanceof Queue queue) { + contextQueues.add(queue); + } + else if (declarable instanceof Binding binding) { + contextBindings.add(binding); + } + }); + }); + } + + +} diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplate.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplate.java new file mode 100644 index 0000000000..d6b89aefa1 --- /dev/null +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplate.java @@ -0,0 +1,494 @@ +/* + * Copyright 2025 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.rabbitmq.client; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Date; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import com.rabbitmq.client.amqp.Connection; +import com.rabbitmq.client.amqp.Consumer; +import com.rabbitmq.client.amqp.Environment; +import com.rabbitmq.client.amqp.Publisher; +import com.rabbitmq.client.amqp.PublisherBuilder; +import com.rabbitmq.client.amqp.Resource; +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.AmqpException; +import org.springframework.amqp.core.AmqpTemplate; +import org.springframework.amqp.core.AsyncAmqpTemplate; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessagePostProcessor; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.core.ReceiveAndReplyCallback; +import org.springframework.amqp.core.ReplyToAddressCallback; +import org.springframework.amqp.rabbit.core.AmqpNackReceivedException; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.amqp.support.converter.SimpleMessageConverter; +import org.springframework.amqp.support.converter.SmartMessageConverter; +import org.springframework.amqp.utils.JavaUtils; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.util.Assert; + +/** + * The {@link AmqpTemplate} for RabbitMQ AMQP 1.0 protocol support. + * A Spring-friendly wrapper around {@link Environment#connectionBuilder()}; + * + * @author Artem Bilan + * + * @since 4.0 + */ +public class RabbitAmqpTemplate implements AsyncAmqpTemplate, InitializingBean, DisposableBean { + + private final Connection connection; + + private final PublisherBuilder publisherBuilder; + + @SuppressWarnings("NullAway.Init") + private Publisher publisher; + + private MessageConverter messageConverter = new SimpleMessageConverter(); + + private @Nullable String defaultExchange; + + private @Nullable String defaultRoutingKey; + + private @Nullable String defaultQueue; + + private @Nullable String defaultReceiveQueue; + + public RabbitAmqpTemplate(Connection amqpConnection) { + this.connection = amqpConnection; + this.publisherBuilder = amqpConnection.publisherBuilder(); + } + + public void setListeners(Resource.StateListener... listeners) { + this.publisherBuilder.listeners(listeners); + } + + public void setPublishTimeout(Duration timeout) { + this.publisherBuilder.publishTimeout(timeout); + } + + /** + * Set a default exchange for publishing. + * Cannot be real default AMQP exchange. + * The {@link #setQueue(String)} is recommended instead. + * Mutually exclusive with {@link #setQueue(String)}. + * @param exchange the default exchange + */ + public void setExchange(String exchange) { + this.defaultExchange = exchange; + } + + /** + * Set a default routing key. + * Mutually exclusive with {@link #setQueue(String)}. + * @param key the default routing key. + */ + public void setKey(String key) { + this.defaultRoutingKey = key; + } + + /** + * Set default queue for publishing. + * Mutually exclusive with {@link #setExchange(String)} and {@link #setKey(String)}. + * @param queue the default queue. + */ + public void setQueue(String queue) { + this.defaultQueue = queue; + } + + /** + * Set a converter for {@link #convertAndSend(Object)} operations. + * @param messageConverter the converter. + */ + public void setMessageConverter(MessageConverter messageConverter) { + this.messageConverter = messageConverter; + } + + /** + * The name of the default queue to receive messages from when none is specified explicitly. + * @param queue the default queue name to use for receive operation. + */ + public void setDefaultReceiveQueue(String queue) { + this.defaultReceiveQueue = queue; + } + + private String getRequiredQueue() throws IllegalStateException { + String name = this.defaultReceiveQueue; + Assert.state(name != null, "No 'queue' specified. Check configuration of this 'RabbitAmqpTemplate'."); + return name; + } + + @Override + public void afterPropertiesSet() { + this.publisher = this.publisherBuilder.build(); + } + + @Override + public void destroy() { + this.publisher.close(); + } + + /** + * Publish a message to the default exchange and routing key (if any) (or queue) configured on this template. + * @param message to publish + * @return the {@link CompletableFuture} as an async result of the message publication. + */ + public CompletableFuture send(Message message) { + return doSend(this.defaultExchange, this.defaultRoutingKey, this.defaultQueue, message); + } + + /** + * Publish the message to the provided queue. + * @param queue to publish + * @param message to publish + * @return the {@link CompletableFuture} as an async result of the message publication. + */ + public CompletableFuture send(String queue, Message message) { + return doSend(null, null, queue, message); + } + + public CompletableFuture send(String exchange, @Nullable String routingKey, Message message) { + return doSend(exchange, routingKey != null ? routingKey : this.defaultRoutingKey, null, message); + } + + private CompletableFuture doSend(@Nullable String exchange, @Nullable String routingKey, + @Nullable String queue, Message message) { + + MessageProperties messageProperties = message.getMessageProperties(); + + com.rabbitmq.client.amqp.Message amqpMessage = + this.publisher.message(message.getBody()) + .contentEncoding(messageProperties.getContentEncoding()) + .contentType(messageProperties.getContentType()) + .messageId(messageProperties.getMessageId()) + .correlationId(messageProperties.getCorrelationId()) + .priority(messageProperties.getPriority().byteValue()) + .replyTo(messageProperties.getReplyTo()); + + com.rabbitmq.client.amqp.Message.MessageAddressBuilder address = amqpMessage.toAddress(); + + Map headers = messageProperties.getHeaders(); + if (!headers.isEmpty()) { + headers.forEach((key, val) -> mapProp(key, val, amqpMessage)); + } + + JavaUtils.INSTANCE + .acceptIfNotNull(messageProperties.getUserId(), + (userId) -> amqpMessage.userId(userId.getBytes(StandardCharsets.UTF_8))) + .acceptIfNotNull(messageProperties.getTimestamp(), + (timestamp) -> amqpMessage.creationTime(timestamp.getTime())) + .acceptIfNotNull(messageProperties.getExpiration(), + (expiration) -> amqpMessage.absoluteExpiryTime(Long.parseLong(expiration))) + .acceptIfNotNull(exchange, address::exchange) + .acceptIfNotNull(routingKey, address::key) + .acceptIfNotNull(queue, address::queue); + + CompletableFuture publishResult = new CompletableFuture<>(); + + this.publisher.publish(address.message(), + (context) -> { + switch (context.status()) { + case ACCEPTED -> publishResult.complete(true); + case REJECTED, RELEASED -> publishResult.completeExceptionally( + new AmqpNackReceivedException("The message was rejected", message)); + } + }); + + return publishResult; + } + + /** + * Publish a message from converted body to the default exchange + * and routing key (if any) (or queue) configured on this template. + * @param message to publish + * @return the {@link CompletableFuture} as an async result of the message publication. + */ + public CompletableFuture convertAndSend(Object message) { + return doConvertAndSend(this.defaultExchange, this.defaultRoutingKey, this.defaultQueue, message, null); + } + + public CompletableFuture convertAndSend(String queue, Object message) { + return doConvertAndSend(null, null, queue, message, null); + } + + public CompletableFuture convertAndSend(String exchange, @Nullable String routingKey, Object message) { + return doConvertAndSend(exchange, routingKey != null ? routingKey : this.defaultRoutingKey, null, message, null); + } + + public CompletableFuture convertAndSend(Object message, + @Nullable MessagePostProcessor messagePostProcessor) { + + return doConvertAndSend(null, null, null, message, messagePostProcessor); + } + + public CompletableFuture convertAndSend(String queue, Object message, + @Nullable MessagePostProcessor messagePostProcessor) { + + return doConvertAndSend(null, null, queue, message, messagePostProcessor); + } + + public CompletableFuture convertAndSend(String exchange, @Nullable String routingKey, Object message, + @Nullable MessagePostProcessor messagePostProcessor) { + + return doConvertAndSend(exchange, routingKey, null, message, messagePostProcessor); + } + + private CompletableFuture doConvertAndSend(@Nullable String exchange, @Nullable String routingKey, + @Nullable String queue, Object data, @Nullable MessagePostProcessor messagePostProcessor) { + + Message message = + data instanceof Message + ? (Message) data + : this.messageConverter.toMessage(data, new MessageProperties()); + if (messagePostProcessor != null) { + message = messagePostProcessor.postProcessMessage(message); + } + return doSend(exchange, routingKey, queue, message); + } + + public CompletableFuture receive() { + return receive(getRequiredQueue()); + } + + @SuppressWarnings("try") + public CompletableFuture receive(String queueName) { + CompletableFuture messageFuture = new CompletableFuture<>(); + + Consumer consumer = + this.connection.consumerBuilder() + .queue(queueName) + .initialCredits(1) + .priority(10) + .messageHandler((context, message) -> { + context.accept(); + messageFuture.complete(fromAmqpMessage(message)); + }) + .build(); + + return messageFuture + .orTimeout(1, TimeUnit.MINUTES) + .whenComplete((message, exception) -> consumer.close()); + } + + public CompletableFuture receiveAndConvert() { + return receiveAndConvert(getRequiredQueue()); + } + + public CompletableFuture receiveAndConvert(String queueName) { + return receive(queueName) + .thenApply(this.messageConverter::fromMessage); + } + + /** + * Receive a message from {@link #setDefaultReceiveQueue(String)} and convert its body + * to the expected type. + * The {@link #setMessageConverter(MessageConverter)} must be an implementation of {@link SmartMessageConverter}. + * @param type the type to covert received result. + * @return the CompletableFuture with a result. + */ + public CompletableFuture receiveAndConvert(ParameterizedTypeReference type) { + return receiveAndConvert(getRequiredQueue(), type); + } + + /** + * Receive a message from {@link #setDefaultReceiveQueue(String)} and convert its body + * to the expected type. + * The {@link #setMessageConverter(MessageConverter)} must be an implementation of {@link SmartMessageConverter}. + * @param queueName the queue to consume message from. + * @param type the type to covert received result. + * @return the CompletableFuture with a result. + */ + @SuppressWarnings("unchecked") + public CompletableFuture receiveAndConvert(String queueName, ParameterizedTypeReference type) { + SmartMessageConverter smartMessageConverter = getRequiredSmartMessageConverter(); + return receive(queueName) + .thenApply((message) -> (T) smartMessageConverter.fromMessage(message, type)); + } + + private SmartMessageConverter getRequiredSmartMessageConverter() throws IllegalStateException { + Assert.state(this.messageConverter instanceof SmartMessageConverter, + "template's message converter must be a SmartMessageConverter"); + return (SmartMessageConverter) this.messageConverter; + } + + public boolean receiveAndReply(ReceiveAndReplyCallback callback) throws AmqpException { + throw new UnsupportedOperationException(); + } + + public boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback) throws AmqpException { + throw new UnsupportedOperationException(); + } + + public boolean receiveAndReply(ReceiveAndReplyCallback callback, String replyExchange, String replyRoutingKey) throws AmqpException { + throw new UnsupportedOperationException(); + } + + public boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback, String replyExchange, String replyRoutingKey) throws AmqpException { + throw new UnsupportedOperationException(); + } + + public boolean receiveAndReply(ReceiveAndReplyCallback callback, ReplyToAddressCallback replyToAddressCallback) throws AmqpException { + throw new UnsupportedOperationException(); + } + + public boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback, ReplyToAddressCallback replyToAddressCallback) throws AmqpException { + throw new UnsupportedOperationException(); + } + + @Override + public CompletableFuture sendAndReceive(Message message) { + throw new UnsupportedOperationException(); + } + + @Override + public CompletableFuture sendAndReceive(String routingKey, Message message) { + throw new UnsupportedOperationException(); + } + + @Override + public CompletableFuture sendAndReceive(String exchange, String routingKey, Message message) { + throw new UnsupportedOperationException(); + } + + @Override + public CompletableFuture convertSendAndReceive(Object object) { + throw new UnsupportedOperationException(); + } + + @Override + public CompletableFuture convertSendAndReceive(String routingKey, Object object) { + throw new UnsupportedOperationException(); + } + + @Override + public CompletableFuture convertSendAndReceive(String exchange, String routingKey, Object object) { + throw new UnsupportedOperationException(); + } + + @Override + public CompletableFuture convertSendAndReceive(Object object, MessagePostProcessor messagePostProcessor) { + throw new UnsupportedOperationException(); + } + + @Override + public CompletableFuture convertSendAndReceive(String routingKey, Object object, MessagePostProcessor messagePostProcessor) { + throw new UnsupportedOperationException(); + } + + @Override + public CompletableFuture convertSendAndReceive(String exchange, String routingKey, Object object, @Nullable MessagePostProcessor messagePostProcessor) { + throw new UnsupportedOperationException(); + } + + @Override + public CompletableFuture convertSendAndReceiveAsType(Object object, ParameterizedTypeReference responseType) { + throw new UnsupportedOperationException(); + } + + @Override + public CompletableFuture convertSendAndReceiveAsType(String routingKey, Object object, ParameterizedTypeReference responseType) { + throw new UnsupportedOperationException(); + } + + @Override + public CompletableFuture convertSendAndReceiveAsType(String exchange, String routingKey, Object object, ParameterizedTypeReference responseType) { + throw new UnsupportedOperationException(); + } + + @Override + public CompletableFuture convertSendAndReceiveAsType(Object object, MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) { + throw new UnsupportedOperationException(); + } + + @Override + public CompletableFuture convertSendAndReceiveAsType(String routingKey, Object object, @Nullable MessagePostProcessor messagePostProcessor, @Nullable ParameterizedTypeReference responseType) { + throw new UnsupportedOperationException(); + } + + @Override + public CompletableFuture convertSendAndReceiveAsType(String exchange, String routingKey, Object object, @Nullable MessagePostProcessor messagePostProcessor, @Nullable ParameterizedTypeReference responseType) { + throw new UnsupportedOperationException(); + } + + private static void mapProp(String key, @Nullable Object val, com.rabbitmq.client.amqp.Message amqpMessage) { + if (val == null) { + return; + } + if (val instanceof String string) { + amqpMessage.property(key, string); + } + else if (val instanceof Long longValue) { + amqpMessage.property(key, longValue); + } + else if (val instanceof Integer intValue) { + amqpMessage.property(key, intValue); + } + else if (val instanceof Short shortValue) { + amqpMessage.property(key, shortValue); + } + else if (val instanceof Byte byteValue) { + amqpMessage.property(key, byteValue); + } + else if (val instanceof Double doubleValue) { + amqpMessage.property(key, doubleValue); + } + else if (val instanceof Float floatValue) { + amqpMessage.property(key, floatValue); + } + else if (val instanceof Character character) { + amqpMessage.property(key, character); + } + else if (val instanceof UUID uuid) { + amqpMessage.property(key, uuid); + } + else if (val instanceof byte[] bytes) { + amqpMessage.property(key, bytes); + } + else if (val instanceof Boolean booleanValue) { + amqpMessage.property(key, booleanValue); + } + } + + private static Message fromAmqpMessage(com.rabbitmq.client.amqp.Message amqpMessage) { + MessageProperties messageProperties = new MessageProperties(); + + JavaUtils.INSTANCE + .acceptIfNotNull(amqpMessage.messageIdAsString(), messageProperties::setMessageId) + .acceptIfNotNull(amqpMessage.userId(), + (usr) -> messageProperties.setUserId(new String(usr, StandardCharsets.UTF_8))) + .acceptIfNotNull(amqpMessage.correlationIdAsString(), messageProperties::setCorrelationId) + .acceptIfNotNull(amqpMessage.contentType(), messageProperties::setContentType) + .acceptIfNotNull(amqpMessage.contentEncoding(), messageProperties::setContentEncoding) + .acceptIfNotNull(amqpMessage.absoluteExpiryTime(), + (exp) -> messageProperties.setExpiration(Long.toString(exp))) + .acceptIfNotNull(amqpMessage.creationTime(), (time) -> messageProperties.setTimestamp(new Date(time))); + + amqpMessage.forEachProperty(messageProperties::setHeader); + + return new Message(amqpMessage.body(), messageProperties); + } + +} diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/package-info.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/package-info.java new file mode 100644 index 0000000000..ae2d6944fd --- /dev/null +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/package-info.java @@ -0,0 +1,5 @@ +/** + * Provides Spring support for RabbitMQ AMQP 1.0 Client. + */ +@org.jspecify.annotations.NullMarked +package org.springframework.amqp.rabbitmq.client; \ No newline at end of file diff --git a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdminTests.java b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdminTests.java new file mode 100644 index 0000000000..6dedc04b65 --- /dev/null +++ b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdminTests.java @@ -0,0 +1,145 @@ +/* + * Copyright 2025 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.rabbitmq.client; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.Declarables; +import org.springframework.amqp.core.DirectExchange; +import org.springframework.amqp.core.Exchange; +import org.springframework.amqp.core.Queue; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Artem Bilan + * + * @since 4.0 + */ +@ContextConfiguration +public class RabbitAmqpAdminTests extends RabbitAmqpTestBase { + + @Autowired + RabbitAmqpTemplate template; + + @Autowired + RabbitAmqpAdmin admin; + + @Autowired + @Qualifier("ds") + Declarables declarables; + + @Test + void verifyBeanDeclarations() { + CompletableFuture publishFutures = + CompletableFuture.allOf( + template.convertAndSend("e1", "k1", "test1"), + template.convertAndSend("e2", "k2", "test2"), + template.convertAndSend("e2", "k2", "test3"), + template.convertAndSend("e3", "k3", "test4"), + template.convertAndSend("e4", "k4", "test5")); + assertThat(publishFutures).succeedsWithin(Duration.ofSeconds(10)); + + assertThat(template.receiveAndConvert("q1")).succeedsWithin(Duration.ofSeconds(10)).isEqualTo("test1"); + assertThat(template.receiveAndConvert("q2")).succeedsWithin(Duration.ofSeconds(10)).isEqualTo("test2"); + assertThat(template.receiveAndConvert("q2")).succeedsWithin(Duration.ofSeconds(10)).isEqualTo("test3"); + assertThat(template.receiveAndConvert("q3")).succeedsWithin(Duration.ofSeconds(10)).isEqualTo("test4"); + assertThat(template.receiveAndConvert("q4")).succeedsWithin(Duration.ofSeconds(10)).isEqualTo("test5"); + + admin.deleteQueue("q1"); + admin.deleteQueue("q2"); + admin.deleteQueue("q3"); + admin.deleteQueue("q4"); + admin.deleteExchange("e1"); + admin.deleteExchange("e2"); + admin.deleteExchange("e3"); + admin.deleteExchange("e4"); + + assertThat(declarables.getDeclarablesByType(Queue.class)) + .hasSize(1) + .extracting(Queue::getName) + .contains("q4"); + assertThat(declarables.getDeclarablesByType(Exchange.class)) + .hasSize(1) + .extracting(Exchange::getName) + .contains("e4"); + assertThat(declarables.getDeclarablesByType(Binding.class)) + .hasSize(1) + .extracting(Binding::getDestination) + .contains("q4"); + } + + @Configuration + public static class Config { + + @Bean + DirectExchange e1() { + return new DirectExchange("e1", false, false); + } + + @Bean + Queue q1() { + return new Queue("q1", false, false, false); + } + + @Bean + Binding b1() { + return BindingBuilder.bind(q1()).to(e1()).with("k1"); + } + + @Bean + Declarables es() { + return new Declarables( + new DirectExchange("e2", false, false), + new DirectExchange("e3", false, false)); + } + + @Bean + Declarables qs() { + return new Declarables( + new Queue("q2", false, false, false), + new Queue("q3", false, false, false)); + } + + @Bean + Declarables bs() { + return new Declarables( + new Binding("q2", Binding.DestinationType.QUEUE, "e2", "k2", null), + new Binding("q3", Binding.DestinationType.QUEUE, "e3", "k3", null)); + } + + @Bean + Declarables ds() { + return new Declarables( + new DirectExchange("e4", false, false), + new Queue("q4", false, false, false), + new Binding("q4", Binding.DestinationType.QUEUE, "e4", "k4", null)); + } + + } + +} diff --git a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplateTests.java b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplateTests.java new file mode 100644 index 0000000000..fbe714cd25 --- /dev/null +++ b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplateTests.java @@ -0,0 +1,107 @@ +/* + * Copyright 2025 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.rabbitmq.client; + +import java.time.Duration; + +import com.rabbitmq.client.amqp.Connection; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.DirectExchange; +import org.springframework.amqp.core.Queue; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Artem Bilan + * + * @since 4.0 + */ +@ContextConfiguration +public class RabbitAmqpTemplateTests extends RabbitAmqpTestBase { + + @Autowired + Connection connection; + + RabbitAmqpTemplate rabbitAmqpTemplate; + + @BeforeEach + void setUp() { + this.rabbitAmqpTemplate = new RabbitAmqpTemplate(this.connection); + this.rabbitAmqpTemplate.afterPropertiesSet(); + } + + @AfterEach + void tearDown() { + this.rabbitAmqpTemplate.destroy(); + } + + @Test + void defaultExchangeAndRoutingKey() { + this.rabbitAmqpTemplate.setExchange("e1"); + this.rabbitAmqpTemplate.setKey("k1"); + + assertThat(this.rabbitAmqpTemplate.convertAndSend("test1")) + .succeedsWithin(Duration.ofSeconds(10)); + + assertThat(this.rabbitAmqpTemplate.receiveAndConvert("q1")) + .succeedsWithin(Duration.ofSeconds(10)) + .isEqualTo("test1"); + } + + @Test + void defaultQueues() { + this.rabbitAmqpTemplate.setQueue("q1"); + this.rabbitAmqpTemplate.setDefaultReceiveQueue("q1"); + + assertThat(this.rabbitAmqpTemplate.convertAndSend("test2")) + .succeedsWithin(Duration.ofSeconds(10)); + + assertThat(this.rabbitAmqpTemplate.receiveAndConvert()) + .succeedsWithin(Duration.ofSeconds(10)) + .isEqualTo("test2"); + } + + @Configuration + static class Config { + + @Bean + DirectExchange e1() { + return new DirectExchange("e1", false, false); + } + + @Bean + Queue q1() { + return new Queue("q1", false, false, false); + } + + @Bean + Binding b1() { + return BindingBuilder.bind(q1()).to(e1()).with("k1"); + } + + } + +} diff --git a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTestBase.java b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTestBase.java new file mode 100644 index 0000000000..bd4eca2c67 --- /dev/null +++ b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTestBase.java @@ -0,0 +1,69 @@ +/* + * Copyright 2025 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.rabbitmq.client; + +import com.rabbitmq.client.amqp.Connection; +import com.rabbitmq.client.amqp.Environment; +import com.rabbitmq.client.amqp.impl.AmqpEnvironmentBuilder; + +import org.springframework.amqp.rabbit.junit.AbstractTestContainerTests; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +/** + * The {@link AbstractTestContainerTests} extension + * + * @author Artem Bilan + * + * @since 4.0 + */ +@SpringJUnitConfig +@DirtiesContext +abstract class RabbitAmqpTestBase extends AbstractTestContainerTests { + + @Configuration + public static class AmqpCommonConfig { + + @Bean + Environment environment() { + return new AmqpEnvironmentBuilder() + .connectionSettings() + .port(amqpPort()) + .environmentBuilder() + .build(); + } + + @Bean + AmqpConnectionFactoryBean connection(Environment environment) { + return new AmqpConnectionFactoryBean(environment); + } + + @Bean + RabbitAmqpAdmin admin(Connection connection) { + return new RabbitAmqpAdmin(connection); + } + + @Bean + RabbitAmqpTemplate rabbitTemplate(Connection connection) { + return new RabbitAmqpTemplate(connection); + } + + } + +} diff --git a/spring-rabbitmq-client/src/test/resources/log4j2-test.xml b/spring-rabbitmq-client/src/test/resources/log4j2-test.xml new file mode 100644 index 0000000000..a4931c982e --- /dev/null +++ b/spring-rabbitmq-client/src/test/resources/log4j2-test.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + From d64596951a92f906575e8e78d906b86087955329 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 26 Feb 2025 13:25:56 -0500 Subject: [PATCH 697/737] Fix `QueueParserIntegrationTests` for `long` of `message_count` --- .../rabbit/config/QueueParserIntegrationTests.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/QueueParserIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/QueueParserIntegrationTests.java index f4fd60665a..fc16e4dc00 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/QueueParserIntegrationTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/config/QueueParserIntegrationTests.java @@ -38,6 +38,8 @@ * @author Dave Syer * @author Gary Russell * @author Gunnar Hillert + * @author Artem Bilan + * * @since 1.0 * */ @@ -47,15 +49,14 @@ public final class QueueParserIntegrationTests { private DefaultListableBeanFactory beanFactory; @BeforeEach - public void setUpDefaultBeanFactory() throws Exception { + public void setUpDefaultBeanFactory() { beanFactory = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory); reader.loadBeanDefinitions(new ClassPathResource(getClass().getSimpleName() + "-context.xml", getClass())); } @Test - public void testArgumentsQueue() throws Exception { - + public void testArgumentsQueue() { Queue queue = beanFactory.getBean("arguments", Queue.class); assertThat(queue).isNotNull(); CachingConnectionFactory connectionFactory = new CachingConnectionFactory( @@ -67,9 +68,9 @@ public void testArgumentsQueue() throws Exception { assertThat(queue.getArguments().get("x-message-ttl")).isEqualTo(100L); template.convertAndSend(queue.getName(), "message"); - await().with().pollInterval(Duration.ofMillis(50)) + await().with().pollInterval(Duration.ofMillis(500)) .until(() -> rabbitAdmin.getQueueProperties("arguments") - .get(RabbitAdmin.QUEUE_MESSAGE_COUNT).equals(0)); + .get(RabbitAdmin.QUEUE_MESSAGE_COUNT).equals(0L)); connectionFactory.destroy(); RabbitAvailableCondition.getBrokerRunning().deleteQueues("arguments"); } From 6f0a3380b82a65d4344c43a9d2383c04db5c0385 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 26 Feb 2025 16:58:51 -0500 Subject: [PATCH 698/737] Move some of `RabbitAmqpTemplate` method up to `AsyncAmqpTemplate` * Improve Client module tests --- .../amqp/core/AsyncAmqpTemplate.java | 68 +++++++++++++++++++ .../rabbitmq/client/RabbitAmqpTemplate.java | 19 ++++++ .../rabbitmq/client/RabbitAmqpAdminTests.java | 6 -- .../client/RabbitAmqpTemplateTests.java | 13 ++++ .../rabbitmq/client/RabbitAmqpTestBase.java | 13 ++++ 5 files changed, 113 insertions(+), 6 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate.java index 3474a65870..5f7556fb70 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate.java @@ -27,11 +27,79 @@ * receive operations using {@link CompletableFuture}s. * * @author Gary Russell + * @author Artem Bilan + * * @since 2.0 * */ public interface AsyncAmqpTemplate { + default CompletableFuture send(Message message) { + throw new UnsupportedOperationException(); + } + + default CompletableFuture send(String queue, Message message) { + throw new UnsupportedOperationException(); + } + + default CompletableFuture send(String exchange, @Nullable String routingKey, Message message) { + throw new UnsupportedOperationException(); + } + + default CompletableFuture convertAndSend(Object message) { + throw new UnsupportedOperationException(); + } + + default CompletableFuture convertAndSend(String queue, Object message) { + throw new UnsupportedOperationException(); + } + + default CompletableFuture convertAndSend(String exchange, @Nullable String routingKey, Object message) { + throw new UnsupportedOperationException(); + } + + default CompletableFuture convertAndSend(Object message, + @Nullable MessagePostProcessor messagePostProcessor) { + + throw new UnsupportedOperationException(); + } + + default CompletableFuture convertAndSend(String queue, Object message, + @Nullable MessagePostProcessor messagePostProcessor) { + + throw new UnsupportedOperationException(); + } + + default CompletableFuture convertAndSend(String exchange, @Nullable String routingKey, Object message, + @Nullable MessagePostProcessor messagePostProcessor) { + + throw new UnsupportedOperationException(); + } + + default CompletableFuture receive() { + throw new UnsupportedOperationException(); + } + + default CompletableFuture receive(String queueName) { + throw new UnsupportedOperationException(); + } + + default CompletableFuture receiveAndConvert() { + throw new UnsupportedOperationException(); + } + + default CompletableFuture receiveAndConvert(String queueName) { + throw new UnsupportedOperationException(); + } + + default CompletableFuture receiveAndConvert(ParameterizedTypeReference type) { + throw new UnsupportedOperationException(); + } + + default CompletableFuture receiveAndConvert(String queueName, ParameterizedTypeReference type) { + throw new UnsupportedOperationException(); + } + /** * Send a message to the default exchange with the default routing key. If the message * contains a correlationId property, it must be unique. diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplate.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplate.java index d6b89aefa1..2ba589bd57 100644 --- a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplate.java +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplate.java @@ -156,7 +156,10 @@ public void destroy() { * @param message to publish * @return the {@link CompletableFuture} as an async result of the message publication. */ + @Override public CompletableFuture send(Message message) { + Assert.state(this.defaultExchange != null || this.defaultQueue != null, + "For send with defaults, an 'exchange' (and optional 'key') or 'queue' must be provided"); return doSend(this.defaultExchange, this.defaultRoutingKey, this.defaultQueue, message); } @@ -166,10 +169,12 @@ public CompletableFuture send(Message message) { * @param message to publish * @return the {@link CompletableFuture} as an async result of the message publication. */ + @Override public CompletableFuture send(String queue, Message message) { return doSend(null, null, queue, message); } + @Override public CompletableFuture send(String exchange, @Nullable String routingKey, Message message) { return doSend(exchange, routingKey != null ? routingKey : this.defaultRoutingKey, null, message); } @@ -226,30 +231,38 @@ private CompletableFuture doSend(@Nullable String exchange, @Nullable S * @param message to publish * @return the {@link CompletableFuture} as an async result of the message publication. */ + @Override public CompletableFuture convertAndSend(Object message) { + Assert.state(this.defaultExchange != null || this.defaultQueue != null, + "For send with defaults, an 'exchange' (and optional 'key') or 'queue' must be provided"); return doConvertAndSend(this.defaultExchange, this.defaultRoutingKey, this.defaultQueue, message, null); } + @Override public CompletableFuture convertAndSend(String queue, Object message) { return doConvertAndSend(null, null, queue, message, null); } + @Override public CompletableFuture convertAndSend(String exchange, @Nullable String routingKey, Object message) { return doConvertAndSend(exchange, routingKey != null ? routingKey : this.defaultRoutingKey, null, message, null); } + @Override public CompletableFuture convertAndSend(Object message, @Nullable MessagePostProcessor messagePostProcessor) { return doConvertAndSend(null, null, null, message, messagePostProcessor); } + @Override public CompletableFuture convertAndSend(String queue, Object message, @Nullable MessagePostProcessor messagePostProcessor) { return doConvertAndSend(null, null, queue, message, messagePostProcessor); } + @Override public CompletableFuture convertAndSend(String exchange, @Nullable String routingKey, Object message, @Nullable MessagePostProcessor messagePostProcessor) { @@ -269,11 +282,13 @@ private CompletableFuture doConvertAndSend(@Nullable String exchange, @ return doSend(exchange, routingKey, queue, message); } + @Override public CompletableFuture receive() { return receive(getRequiredQueue()); } @SuppressWarnings("try") + @Override public CompletableFuture receive(String queueName) { CompletableFuture messageFuture = new CompletableFuture<>(); @@ -293,10 +308,12 @@ public CompletableFuture receive(String queueName) { .whenComplete((message, exception) -> consumer.close()); } + @Override public CompletableFuture receiveAndConvert() { return receiveAndConvert(getRequiredQueue()); } + @Override public CompletableFuture receiveAndConvert(String queueName) { return receive(queueName) .thenApply(this.messageConverter::fromMessage); @@ -309,6 +326,7 @@ public CompletableFuture receiveAndConvert(String queueName) { * @param type the type to covert received result. * @return the CompletableFuture with a result. */ + @Override public CompletableFuture receiveAndConvert(ParameterizedTypeReference type) { return receiveAndConvert(getRequiredQueue(), type); } @@ -322,6 +340,7 @@ public CompletableFuture receiveAndConvert(ParameterizedTypeReference * @return the CompletableFuture with a result. */ @SuppressWarnings("unchecked") + @Override public CompletableFuture receiveAndConvert(String queueName, ParameterizedTypeReference type) { SmartMessageConverter smartMessageConverter = getRequiredSmartMessageConverter(); return receive(queueName) diff --git a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdminTests.java b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdminTests.java index 6dedc04b65..8a95bb0233 100644 --- a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdminTests.java +++ b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdminTests.java @@ -43,12 +43,6 @@ @ContextConfiguration public class RabbitAmqpAdminTests extends RabbitAmqpTestBase { - @Autowired - RabbitAmqpTemplate template; - - @Autowired - RabbitAmqpAdmin admin; - @Autowired @Qualifier("ds") Declarables declarables; diff --git a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplateTests.java b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplateTests.java index fbe714cd25..bff14523f6 100644 --- a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplateTests.java +++ b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplateTests.java @@ -26,6 +26,7 @@ import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.DirectExchange; +import org.springframework.amqp.core.Message; import org.springframework.amqp.core.Queue; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -33,6 +34,7 @@ import org.springframework.test.context.ContextConfiguration; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * @author Artem Bilan @@ -58,6 +60,17 @@ void tearDown() { this.rabbitAmqpTemplate.destroy(); } + @Test + void illegalStateOnNoDefaults() { + assertThatIllegalStateException() + .isThrownBy(() -> this.template.send(new Message(new byte[0]))) + .withMessage("For send with defaults, an 'exchange' (and optional 'key') or 'queue' must be provided"); + + assertThatIllegalStateException() + .isThrownBy(() -> this.template.convertAndSend(new byte[0])) + .withMessage("For send with defaults, an 'exchange' (and optional 'key') or 'queue' must be provided"); + } + @Test void defaultExchangeAndRoutingKey() { this.rabbitAmqpTemplate.setExchange("e1"); diff --git a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTestBase.java b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTestBase.java index bd4eca2c67..4d7284126c 100644 --- a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTestBase.java +++ b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTestBase.java @@ -21,6 +21,7 @@ import com.rabbitmq.client.amqp.impl.AmqpEnvironmentBuilder; import org.springframework.amqp.rabbit.junit.AbstractTestContainerTests; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.test.annotation.DirtiesContext; @@ -37,6 +38,18 @@ @DirtiesContext abstract class RabbitAmqpTestBase extends AbstractTestContainerTests { + @Autowired + protected Environment environment; + + @Autowired + protected Connection connection; + + @Autowired + protected RabbitAmqpAdmin admin; + + @Autowired + protected RabbitAmqpTemplate template; + @Configuration public static class AmqpCommonConfig { From 5be96cb92066dc2c774317ece9f3de9b8e1776c2 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 28 Feb 2025 17:34:55 -0500 Subject: [PATCH 699/737] GH-2994: Add `RabbitAmqpListenerContainer` infrastructure Related to: https://github.com/spring-projects/spring-amqp/issues/2994 * Add `RabbitAmqpMessageListener` for RabbitMQ AMQP 1.0 native message consumption * Add `RabbitAmqpMessageListenerAdapter` for `@RabbitLister` API * Add `RabbitAmqpListenerContainer` and respective `RabbitAmqpListenerContainerFactory` * Add `AmqpAcknowledgment` as a general abstraction. In the `RabbitAmqpListenerContainer` delegates to the `Consumer.Context` for manual settlement * Extract `RabbitAmqpUtils` for conversion to/from AMQP 1.0 native message * Add convenient `ContainerUtils.isImmediateAcknowledge()` and `ContainerUtils.isAmqpReject()` utilities * Expose `AmqpAcknowledgment` as a `MessageProperties.amqpAcknowledgment` for generic `MessageListener` use-cases * Remove `io.micrometer` dependecies from the `spring-rabbitmq-client` module since metrics and observation handled thoroughly in the `com.rabbitmq.client:amqp-client` Not tests for the listener yet. Therefore no fixing for the issue. --- build.gradle | 5 - .../amqp/core/AmqpAcknowledgment.java | 58 +++ .../amqp/core/MessageProperties.java | 21 + .../AbstractMessageListenerContainer.java | 2 +- .../listener/support/ContainerUtils.java | 47 ++- .../rabbitmq/client/RabbitAmqpTemplate.java | 94 +---- .../amqp/rabbitmq/client/RabbitAmqpUtils.java | 145 +++++++ .../RabbitAmqpListenerContainerFactory.java | 97 +++++ .../rabbitmq/client/config/package-info.java | 5 + .../listener/RabbitAmqpListenerContainer.java | 392 ++++++++++++++++++ .../listener/RabbitAmqpMessageListener.java | 42 ++ .../RabbitAmqpMessageListenerAdapter.java | 72 ++++ .../client/listener/package-info.java | 5 + 13 files changed, 888 insertions(+), 97 deletions(-) create mode 100644 spring-amqp/src/main/java/org/springframework/amqp/core/AmqpAcknowledgment.java create mode 100644 spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpUtils.java create mode 100644 spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/config/RabbitAmqpListenerContainerFactory.java create mode 100644 spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/config/package-info.java create mode 100644 spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerContainer.java create mode 100644 spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListener.java create mode 100644 spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListenerAdapter.java create mode 100644 spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/package-info.java diff --git a/build.gradle b/build.gradle index f226c681b4..3f06a8d37b 100644 --- a/build.gradle +++ b/build.gradle @@ -479,7 +479,6 @@ project('spring-rabbitmq-client') { dependencies { api project(':spring-rabbit') api "com.rabbitmq.client:amqp-client:$rabbitmqAmqpClientVersion" - api 'io.micrometer:micrometer-observation' testApi project(':spring-rabbit-junit') @@ -488,10 +487,6 @@ project('spring-rabbitmq-client') { testImplementation 'org.testcontainers:rabbitmq' testImplementation 'org.testcontainers:junit-jupiter' testImplementation 'org.apache.logging.log4j:log4j-slf4j-impl' - testImplementation 'io.micrometer:micrometer-observation-test' - testImplementation 'io.micrometer:micrometer-tracing-bridge-brave' - testImplementation 'io.micrometer:micrometer-tracing-test' - testImplementation 'io.micrometer:micrometer-tracing-integration-test' } } diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpAcknowledgment.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpAcknowledgment.java new file mode 100644 index 0000000000..8b9b94d047 --- /dev/null +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AmqpAcknowledgment.java @@ -0,0 +1,58 @@ +/* + * Copyright 2025 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.core; + +/** + * An abstraction over acknowledgments. + * + * @author Artem Bilan + * + * @since 4.0 + */ +@FunctionalInterface +public interface AmqpAcknowledgment { + + /** + * Acknowledge the message. + * @param status the status. + */ + void acknowledge(Status status); + + default void acknowledge() { + acknowledge(Status.ACCEPT); + } + + enum Status { + + /** + * Mark the message as accepted. + */ + ACCEPT, + + /** + * Mark the message as rejected. + */ + REJECT, + + /** + * Reject the message and requeue so that it will be redelivered. + */ + REQUEUE + + } + +} diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java index 758e03af70..08c55e57e6 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java @@ -157,6 +157,8 @@ public class MessageProperties implements Serializable { private transient @Nullable Object targetBean; + private transient @Nullable AmqpAcknowledgment amqpAcknowledgment; + public void setHeader(String key, Object value) { this.headers.put(key, value); } @@ -641,6 +643,25 @@ public void setProjectionUsed(boolean projectionUsed) { } } + /** + * Return the {@link AmqpAcknowledgment} for consumer if any. + * @return the {@link AmqpAcknowledgment} for consumer if any. + * @since 4.0 + */ + public @Nullable AmqpAcknowledgment getAmqpAcknowledgment() { + return this.amqpAcknowledgment; + } + + /** + * Set an {@link AmqpAcknowledgment} for manual acks in the target message processor. + * This is only in-application a consumer side logic. + * @param amqpAcknowledgment the {@link AmqpAcknowledgment} to use in the application. + * @since 4.0 + */ + public void setAmqpAcknowledgment(AmqpAcknowledgment amqpAcknowledgment) { + this.amqpAcknowledgment = amqpAcknowledgment; + } + @Override // NOSONAR complexity public int hashCode() { final int prime = 31; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index 6a24f0fb93..130428dd4e 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -752,7 +752,7 @@ protected boolean isNoLocal() { * to be sent to the dead letter exchange. Setting to false causes all rejections to not * be requeued. When true, the default can be overridden by the listener throwing an * {@link AmqpRejectAndDontRequeueException}. Default true. - * @param defaultRequeueRejected true to reject by default. + * @param defaultRequeueRejected true to requeue by default. */ public void setDefaultRequeueRejected(boolean defaultRequeueRejected) { this.defaultRequeueRejected = defaultRequeueRejected; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/support/ContainerUtils.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/support/ContainerUtils.java index 06d83ebc02..f57094799a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/support/ContainerUtils.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/support/ContainerUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2022 the original author or authors. + * Copyright 2018-2025 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. @@ -17,8 +17,10 @@ package org.springframework.amqp.rabbit.listener.support; import org.apache.commons.logging.Log; +import org.jspecify.annotations.Nullable; import org.springframework.amqp.AmqpRejectAndDontRequeueException; +import org.springframework.amqp.ImmediateAcknowledgeAmqpException; import org.springframework.amqp.ImmediateRequeueAmqpException; import org.springframework.amqp.rabbit.listener.exception.MessageRejectedWhileStoppingException; @@ -59,7 +61,11 @@ else if (t instanceof ImmediateRequeueAmqpException) { shouldRequeue = true; break; } - t = t.getCause(); + Throwable cause = t.getCause(); + if (cause == t) { + break; + } + t = cause; } if (logger.isDebugEnabled()) { logger.debug("Rejecting messages (requeue=" + shouldRequeue + ")"); @@ -75,8 +81,41 @@ else if (t instanceof ImmediateRequeueAmqpException) { * @since 2.2 */ public static boolean isRejectManual(Throwable ex) { - return ex instanceof AmqpRejectAndDontRequeueException aradrex - && aradrex.isRejectManual(); + AmqpRejectAndDontRequeueException amqpRejectAndDontRequeueException = + findInCause(ex, AmqpRejectAndDontRequeueException.class); + return amqpRejectAndDontRequeueException != null && amqpRejectAndDontRequeueException.isRejectManual(); + } + + /** + * Return true for {@link ImmediateAcknowledgeAmqpException}. + * @param ex the exception to traverse. + * @return true if an {@link ImmediateAcknowledgeAmqpException} is present in the cause chain. + * @since 4.0 + */ + public static boolean isImmediateAcknowledge(Throwable ex) { + return findInCause(ex, ImmediateAcknowledgeAmqpException.class) != null; + } + + /** + * Return true for {@link AmqpRejectAndDontRequeueException}. + * @param ex the exception to traverse. + * @return true if an {@link AmqpRejectAndDontRequeueException} is present in the cause chain. + * @since 4.0 + */ + public static boolean isAmqpReject(Throwable ex) { + return findInCause(ex, AmqpRejectAndDontRequeueException.class) != null; + } + + @SuppressWarnings("unchecked") + private static @Nullable T findInCause(Throwable throwable, Class exceptionToFind) { + if (exceptionToFind.isAssignableFrom(throwable.getClass())) { + return (T) throwable; + } + Throwable cause = throwable.getCause(); + if (cause == null || cause == throwable) { + return null; + } + return findInCause(cause, exceptionToFind); } } diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplate.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplate.java index 2ba589bd57..c2a6eba633 100644 --- a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplate.java +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplate.java @@ -16,11 +16,7 @@ package org.springframework.amqp.rabbitmq.client; -import java.nio.charset.StandardCharsets; import java.time.Duration; -import java.util.Date; -import java.util.Map; -import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -182,38 +178,20 @@ public CompletableFuture send(String exchange, @Nullable String routing private CompletableFuture doSend(@Nullable String exchange, @Nullable String routingKey, @Nullable String queue, Message message) { - MessageProperties messageProperties = message.getMessageProperties(); - - com.rabbitmq.client.amqp.Message amqpMessage = - this.publisher.message(message.getBody()) - .contentEncoding(messageProperties.getContentEncoding()) - .contentType(messageProperties.getContentType()) - .messageId(messageProperties.getMessageId()) - .correlationId(messageProperties.getCorrelationId()) - .priority(messageProperties.getPriority().byteValue()) - .replyTo(messageProperties.getReplyTo()); - + com.rabbitmq.client.amqp.Message amqpMessage = this.publisher.message(); com.rabbitmq.client.amqp.Message.MessageAddressBuilder address = amqpMessage.toAddress(); - - Map headers = messageProperties.getHeaders(); - if (!headers.isEmpty()) { - headers.forEach((key, val) -> mapProp(key, val, amqpMessage)); - } - JavaUtils.INSTANCE - .acceptIfNotNull(messageProperties.getUserId(), - (userId) -> amqpMessage.userId(userId.getBytes(StandardCharsets.UTF_8))) - .acceptIfNotNull(messageProperties.getTimestamp(), - (timestamp) -> amqpMessage.creationTime(timestamp.getTime())) - .acceptIfNotNull(messageProperties.getExpiration(), - (expiration) -> amqpMessage.absoluteExpiryTime(Long.parseLong(expiration))) .acceptIfNotNull(exchange, address::exchange) .acceptIfNotNull(routingKey, address::key) .acceptIfNotNull(queue, address::queue); + amqpMessage = address.message(); + + RabbitAmqpUtils.toAmqpMessage(message, amqpMessage); + CompletableFuture publishResult = new CompletableFuture<>(); - this.publisher.publish(address.message(), + this.publisher.publish(amqpMessage, (context) -> { switch (context.status()) { case ACCEPTED -> publishResult.complete(true); @@ -299,7 +277,7 @@ public CompletableFuture receive(String queueName) { .priority(10) .messageHandler((context, message) -> { context.accept(); - messageFuture.complete(fromAmqpMessage(message)); + messageFuture.complete(RabbitAmqpUtils.fromAmqpMessage(message, null)); }) .build(); @@ -452,62 +430,4 @@ public CompletableFuture convertSendAndReceiveAsType(String exchange, Str throw new UnsupportedOperationException(); } - private static void mapProp(String key, @Nullable Object val, com.rabbitmq.client.amqp.Message amqpMessage) { - if (val == null) { - return; - } - if (val instanceof String string) { - amqpMessage.property(key, string); - } - else if (val instanceof Long longValue) { - amqpMessage.property(key, longValue); - } - else if (val instanceof Integer intValue) { - amqpMessage.property(key, intValue); - } - else if (val instanceof Short shortValue) { - amqpMessage.property(key, shortValue); - } - else if (val instanceof Byte byteValue) { - amqpMessage.property(key, byteValue); - } - else if (val instanceof Double doubleValue) { - amqpMessage.property(key, doubleValue); - } - else if (val instanceof Float floatValue) { - amqpMessage.property(key, floatValue); - } - else if (val instanceof Character character) { - amqpMessage.property(key, character); - } - else if (val instanceof UUID uuid) { - amqpMessage.property(key, uuid); - } - else if (val instanceof byte[] bytes) { - amqpMessage.property(key, bytes); - } - else if (val instanceof Boolean booleanValue) { - amqpMessage.property(key, booleanValue); - } - } - - private static Message fromAmqpMessage(com.rabbitmq.client.amqp.Message amqpMessage) { - MessageProperties messageProperties = new MessageProperties(); - - JavaUtils.INSTANCE - .acceptIfNotNull(amqpMessage.messageIdAsString(), messageProperties::setMessageId) - .acceptIfNotNull(amqpMessage.userId(), - (usr) -> messageProperties.setUserId(new String(usr, StandardCharsets.UTF_8))) - .acceptIfNotNull(amqpMessage.correlationIdAsString(), messageProperties::setCorrelationId) - .acceptIfNotNull(amqpMessage.contentType(), messageProperties::setContentType) - .acceptIfNotNull(amqpMessage.contentEncoding(), messageProperties::setContentEncoding) - .acceptIfNotNull(amqpMessage.absoluteExpiryTime(), - (exp) -> messageProperties.setExpiration(Long.toString(exp))) - .acceptIfNotNull(amqpMessage.creationTime(), (time) -> messageProperties.setTimestamp(new Date(time))); - - amqpMessage.forEachProperty(messageProperties::setHeader); - - return new Message(amqpMessage.body(), messageProperties); - } - } diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpUtils.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpUtils.java new file mode 100644 index 0000000000..340b82d930 --- /dev/null +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpUtils.java @@ -0,0 +1,145 @@ +/* + * Copyright 2025 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.rabbitmq.client; + +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.Map; +import java.util.UUID; + +import com.rabbitmq.client.amqp.Consumer; +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.utils.JavaUtils; + +/** + * The utilities for RabbitMQ AMQP 1.0 protocol API. + */ +public final class RabbitAmqpUtils { + + /** + * Convert {@link com.rabbitmq.client.amqp.Message} into {@link Message}. + * @param amqpMessage the {@link com.rabbitmq.client.amqp.Message} convert from. + * @param context the {@link Consumer.Context} for manual message settlement. + * @return the {@link Message} mapped from a {@link com.rabbitmq.client.amqp.Message}. + */ + public static Message fromAmqpMessage(com.rabbitmq.client.amqp.Message amqpMessage, + Consumer.@Nullable Context context) { + + MessageProperties messageProperties = new MessageProperties(); + + JavaUtils.INSTANCE + .acceptIfNotNull(amqpMessage.messageIdAsString(), messageProperties::setMessageId) + .acceptIfNotNull(amqpMessage.userId(), + (usr) -> messageProperties.setUserId(new String(usr, StandardCharsets.UTF_8))) + .acceptIfNotNull(amqpMessage.correlationIdAsString(), messageProperties::setCorrelationId) + .acceptIfNotNull(amqpMessage.contentType(), messageProperties::setContentType) + .acceptIfNotNull(amqpMessage.contentEncoding(), messageProperties::setContentEncoding) + .acceptIfNotNull(amqpMessage.absoluteExpiryTime(), + (exp) -> messageProperties.setExpiration(Long.toString(exp))) + .acceptIfNotNull(amqpMessage.creationTime(), (time) -> messageProperties.setTimestamp(new Date(time))); + + amqpMessage.forEachProperty(messageProperties::setHeader); + + if (context != null) { + messageProperties.setAmqpAcknowledgment((status) -> { + switch (status) { + case ACCEPT -> context.accept(); + case REJECT -> context.discard(); + case REQUEUE -> context.requeue(); + } + }); + } + + return new Message(amqpMessage.body(), messageProperties); + } + + /** + * Convert {@link com.rabbitmq.client.amqp.Message} into {@link Message}. + * @param amqpMessage the {@link com.rabbitmq.client.amqp.Message} convert from. + */ + public static void toAmqpMessage(Message message, com.rabbitmq.client.amqp.Message amqpMessage) { + MessageProperties messageProperties = message.getMessageProperties(); + + amqpMessage + .body(message.getBody()) + .contentEncoding(messageProperties.getContentEncoding()) + .contentType(messageProperties.getContentType()) + .messageId(messageProperties.getMessageId()) + .correlationId(messageProperties.getCorrelationId()) + .priority(messageProperties.getPriority().byteValue()) + .replyTo(messageProperties.getReplyTo()); + + Map headers = messageProperties.getHeaders(); + if (!headers.isEmpty()) { + headers.forEach((key, val) -> mapProp(key, val, amqpMessage)); + } + + JavaUtils.INSTANCE + .acceptIfNotNull(messageProperties.getUserId(), + (userId) -> amqpMessage.userId(userId.getBytes(StandardCharsets.UTF_8))) + .acceptIfNotNull(messageProperties.getTimestamp(), + (timestamp) -> amqpMessage.creationTime(timestamp.getTime())) + .acceptIfNotNull(messageProperties.getExpiration(), + (expiration) -> amqpMessage.absoluteExpiryTime(Long.parseLong(expiration))); + } + + private static void mapProp(String key, @Nullable Object val, com.rabbitmq.client.amqp.Message amqpMessage) { + if (val == null) { + return; + } + if (val instanceof String string) { + amqpMessage.property(key, string); + } + else if (val instanceof Long longValue) { + amqpMessage.property(key, longValue); + } + else if (val instanceof Integer intValue) { + amqpMessage.property(key, intValue); + } + else if (val instanceof Short shortValue) { + amqpMessage.property(key, shortValue); + } + else if (val instanceof Byte byteValue) { + amqpMessage.property(key, byteValue); + } + else if (val instanceof Double doubleValue) { + amqpMessage.property(key, doubleValue); + } + else if (val instanceof Float floatValue) { + amqpMessage.property(key, floatValue); + } + else if (val instanceof Character character) { + amqpMessage.property(key, character); + } + else if (val instanceof UUID uuid) { + amqpMessage.property(key, uuid); + } + else if (val instanceof byte[] bytes) { + amqpMessage.property(key, bytes); + } + else if (val instanceof Boolean booleanValue) { + amqpMessage.property(key, booleanValue); + } + } + + private RabbitAmqpUtils() { + } + +} diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/config/RabbitAmqpListenerContainerFactory.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/config/RabbitAmqpListenerContainerFactory.java new file mode 100644 index 0000000000..926580f5a0 --- /dev/null +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/config/RabbitAmqpListenerContainerFactory.java @@ -0,0 +1,97 @@ +/* + * Copyright 2021-2025 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.rabbitmq.client.config; + +import com.rabbitmq.client.amqp.Connection; +import org.aopalliance.aop.Advice; +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.rabbit.config.BaseRabbitListenerContainerFactory; +import org.springframework.amqp.rabbit.config.ContainerCustomizer; +import org.springframework.amqp.rabbit.listener.MethodRabbitListenerEndpoint; +import org.springframework.amqp.rabbit.listener.RabbitListenerEndpoint; +import org.springframework.amqp.rabbitmq.client.listener.RabbitAmqpListenerContainer; +import org.springframework.amqp.rabbitmq.client.listener.RabbitAmqpMessageListenerAdapter; +import org.springframework.amqp.utils.JavaUtils; + +/** + * Factory for {@link RabbitAmqpListenerContainer}. + * To use it as default one, has to be configured with a + * {@link org.springframework.amqp.rabbit.annotation.RabbitListenerAnnotationBeanPostProcessor#DEFAULT_RABBIT_LISTENER_CONTAINER_FACTORY_BEAN_NAME}. + * + * @author Artem Bilan + * + * @since 4.0 + * + */ +public class RabbitAmqpListenerContainerFactory + extends BaseRabbitListenerContainerFactory { + + private final Connection connection; + + private @Nullable ContainerCustomizer containerCustomizer; + + /** + * Construct an instance using the provided amqpConnection. + * @param amqpConnection the connection. + */ + public RabbitAmqpListenerContainerFactory(Connection amqpConnection) { + this.connection = amqpConnection; + } + + /** + * Set a {@link ContainerCustomizer} that is invoked after a container is created and + * configured to enable further customization of the container. + * @param containerCustomizer the customizer. + */ + public void setContainerCustomizer(ContainerCustomizer containerCustomizer) { + this.containerCustomizer = containerCustomizer; + } + + @Override + public RabbitAmqpListenerContainer createListenerContainer(@Nullable RabbitListenerEndpoint endpoint) { + if (endpoint instanceof MethodRabbitListenerEndpoint methodRabbitListenerEndpoint) { + methodRabbitListenerEndpoint.setAdapterProvider( + (batch, bean, method, returnExceptions, errorHandler, batchingStrategy) -> + new RabbitAmqpMessageListenerAdapter(bean, method, returnExceptions, errorHandler)); + } + RabbitAmqpListenerContainer container = createContainerInstance(); + Advice[] adviceChain = getAdviceChain(); + JavaUtils.INSTANCE + .acceptIfNotNull(adviceChain, container::setAdviceChain) + .acceptIfNotNull(getDefaultRequeueRejected(), container::setDefaultRequeue); + + applyCommonOverrides(endpoint, container); + + if (endpoint != null) { + JavaUtils.INSTANCE + .acceptIfNotNull(endpoint.getAckMode(), + (ackMode) -> container.setAutoSettle(!ackMode.isManual())) + .acceptIfNotNull(endpoint.getConcurrency(), + (concurrency) -> container.setConsumersPerQueue(Integer.parseInt(concurrency))); + } + if (this.containerCustomizer != null) { + this.containerCustomizer.configure(container); + } + return container; + } + + protected RabbitAmqpListenerContainer createContainerInstance() { + return new RabbitAmqpListenerContainer(this.connection); + } + +} diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/config/package-info.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/config/package-info.java new file mode 100644 index 0000000000..3b874dd25f --- /dev/null +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/config/package-info.java @@ -0,0 +1,5 @@ +/** + * Provides classes for Spring application context support. + */ +@org.jspecify.annotations.NullMarked +package org.springframework.amqp.rabbitmq.client.config; \ No newline at end of file diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerContainer.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerContainer.java new file mode 100644 index 0000000000..479af40f89 --- /dev/null +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerContainer.java @@ -0,0 +1,392 @@ +/* + * Copyright 2025 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.rabbitmq.client.listener; + +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import com.rabbitmq.client.amqp.Connection; +import com.rabbitmq.client.amqp.Consumer; +import com.rabbitmq.client.amqp.Resource; +import org.aopalliance.aop.Advice; +import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.AmqpRejectAndDontRequeueException; +import org.springframework.amqp.core.AcknowledgeMode; +import org.springframework.amqp.core.AmqpAcknowledgment; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageListener; +import org.springframework.amqp.rabbit.listener.ConditionalRejectingErrorHandler; +import org.springframework.amqp.rabbit.listener.MessageListenerContainer; +import org.springframework.amqp.rabbit.listener.support.ContainerUtils; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpUtils; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.core.log.LogAccessor; +import org.springframework.util.Assert; +import org.springframework.util.ErrorHandler; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.ObjectUtils; + +/** + * A listener container for RabbitMQ AMQP 1.0 Consumer. + * + * @author Artem Bilan + * + * @since 4.0 + * + */ +public class RabbitAmqpListenerContainer implements MessageListenerContainer { + + private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(RabbitAmqpListenerContainer.class)); + + private final Lock lock = new ReentrantLock(); + + private final Connection connection; + + private final MultiValueMap queueToConsumers = new LinkedMultiValueMap<>(); + + private String @Nullable [] queues; + + private Advice @Nullable [] adviceChain; + + private int initialCredits = 100; + + private int priority; + + private Resource.StateListener @Nullable [] stateListeners; + + private boolean autoSettle = true; + + private boolean defaultRequeue = true; + + private int consumersPerQueue = 1; + + private @Nullable MessageListener messageListener; + + private ErrorHandler errorHandler = new ConditionalRejectingErrorHandler(); + + private boolean autoStartup = true; + + private @Nullable String listenerId; + + private Duration gracefulShutdownPeriod = Duration.ofSeconds(30); + + /** + * Construct an instance using the provided connection. + * @param connection to use. + */ + public RabbitAmqpListenerContainer(Connection connection) { + this.connection = connection; + } + + @Override + public void setQueueNames(String... queueNames) { + this.queues = Arrays.copyOf(queueNames, queueNames.length); + } + + public void setInitialCredits(int initialCredits) { + this.initialCredits = initialCredits; + } + + public void setPriority(int priority) { + this.priority = priority; + } + + public void setStateListeners(Resource.StateListener... stateListeners) { + this.stateListeners = Arrays.copyOf(stateListeners, stateListeners.length); + } + + @Override + public void setAutoStartup(boolean autoStart) { + this.autoStartup = autoStart; + } + + @Override + public boolean isAutoStartup() { + return this.autoStartup; + } + + /** + * Set an advice chain to apply to the listener. + * @param advices the advice chain. + * @since 2.4.5 + */ + public void setAdviceChain(Advice... advices) { + Assert.notNull(advices, "'advices' cannot be null"); + Assert.noNullElements(advices, "'advices' cannot have null elements"); + this.adviceChain = Arrays.copyOf(advices, advices.length); + } + + /** + * Set to {@code false} to propagate a + * {@link org.springframework.amqp.core.MessageProperties#setAmqpAcknowledgment(AmqpAcknowledgment)} + * for target {@link MessageListener} manual settlement. + * In case of {@link RabbitAmqpMessageListener}, the native {@link Consumer.Context} + * should be used for manual settlement. + * @param autoSettle to call {@link Consumer.Context#accept()} automatically. + */ + public void setAutoSettle(boolean autoSettle) { + this.autoSettle = autoSettle; + } + + /** + * Set the default behavior when a message processing has failed. + * When true, messages will be requeued, when false, they will be discarded. + * When true, the default can be overridden by the listener throwing an + * {@link AmqpRejectAndDontRequeueException}. Default true. + * @param defaultRequeue true to requeue by default. + */ + public void setDefaultRequeue(boolean defaultRequeue) { + this.defaultRequeue = defaultRequeue; + } + + public void setGracefulShutdownPeriod(Duration gracefulShutdownPeriod) { + this.gracefulShutdownPeriod = gracefulShutdownPeriod; + } + + /** + * Each queue runs in its own consumer; set this property to create multiple + * consumers for each queue. + * Can be treated as {@code concurrency}, but per queue. + * @param consumersPerQueue the consumers per queue. + */ + public void setConsumersPerQueue(int consumersPerQueue) { + this.consumersPerQueue = consumersPerQueue; + } + + public void setErrorHandler(ErrorHandler errorHandler) { + this.errorHandler = errorHandler; + } + + @Override + public void setListenerId(String id) { + this.listenerId = id; + } + + @Override + public void setupMessageListener(MessageListener messageListener) { + this.messageListener = messageListener; + if (!ObjectUtils.isEmpty(this.adviceChain)) { + ProxyFactory factory = new ProxyFactory(messageListener); + for (Advice advice : this.adviceChain) { + factory.addAdvisor(new DefaultPointcutAdvisor(advice)); + } + factory.setInterfaces(messageListener.getClass().getInterfaces()); + this.messageListener = (MessageListener) factory.getProxy(getClass().getClassLoader()); + } + } + + @Override + public @Nullable Object getMessageListener() { + return this.messageListener; + } + + @Override + public void afterPropertiesSet() { + Assert.state(this.queues != null, "At least one queue has to be provided for consuming."); + Assert.state(this.messageListener != null, "The 'messageListener' must be provided."); + + this.messageListener.containerAckMode(this.autoSettle ? AcknowledgeMode.AUTO : AcknowledgeMode.MANUAL); + } + + @Override + public boolean isRunning() { + this.lock.lock(); + try { + return !this.queueToConsumers.isEmpty(); + } + finally { + this.lock.unlock(); + } + } + + @Override + @SuppressWarnings("NullAway") // Dataflow analysis limitation + public void start() { + this.lock.lock(); + try { + if (this.queueToConsumers.isEmpty()) { + for (String queue : this.queues) { + for (int i = 0; i < this.consumersPerQueue; i++) { + Consumer consumer = + this.connection.consumerBuilder() + .queue(queue) + .priority(this.priority) + .initialCredits(this.initialCredits) + .listeners(this.stateListeners) + .messageHandler(this::invokeListener) + .build(); + this.queueToConsumers.add(queue, consumer); + } + } + } + } + finally { + this.lock.unlock(); + } + } + + private void invokeListener(Consumer.Context context, com.rabbitmq.client.amqp.Message amqpMessage) { + try { + doInvokeListener(context, amqpMessage); + if (this.autoSettle) { + context.accept(); + } + } + catch (Exception ex) { + if (!handleSpecialErrors(ex, context)) { + try { + this.errorHandler.handleError(ex); + // If error handler does not re-throw an exception, treat it as a successful processing result. + context.accept(); + } + catch (Exception rethrow) { + if (!handleSpecialErrors(rethrow, context)) { + if (this.defaultRequeue) { + context.requeue(); + } + else { + context.discard(); + } + LOG.error(rethrow, () -> + "The 'errorHandler' has thrown an exception. The '" + amqpMessage + "' is " + + (this.defaultRequeue ? "re-queued." : "discarded.")); + } + } + } + } + } + + @SuppressWarnings("NullAway") // Dataflow analysis limitation + private void doInvokeListener(Consumer.Context context, com.rabbitmq.client.amqp.Message amqpMessage) { + Consumer.@Nullable Context contextToUse = this.autoSettle ? null : context; + if (this.messageListener instanceof RabbitAmqpMessageListener amqpMessageListener) { + amqpMessageListener.onAmqpMessage(amqpMessage, contextToUse); + } + else { + Message message = RabbitAmqpUtils.fromAmqpMessage(amqpMessage, contextToUse); + this.messageListener.onMessage(message); + } + } + + private boolean handleSpecialErrors(Exception ex, Consumer.Context context) { + if (ContainerUtils.shouldRequeue(this.defaultRequeue, ex, LOG.getLog())) { + context.requeue(); + return true; + } + if (ContainerUtils.isAmqpReject(ex)) { + context.discard(); + return true; + } + if (ContainerUtils.isImmediateAcknowledge(ex)) { + context.accept(); + return true; + } + return false; + } + + @Override + public void stop() { + stop(() -> { + }); + } + + @Override + @SuppressWarnings("unchecked") + public void stop(Runnable callback) { + this.lock.lock(); + try { + CompletableFuture[] completableFutures = + this.queueToConsumers.values().stream() + .flatMap(List::stream) + .peek(Consumer::pause) + .map((consumer) -> + CompletableFuture.supplyAsync(() -> { + try (consumer) { + while (consumer.unsettledMessageCount() > 0) { + Thread.sleep(100); + } + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new RuntimeException(ex); + } + return null; + })) + .toArray(CompletableFuture[]::new); + + CompletableFuture.allOf(completableFutures) + .orTimeout(this.gracefulShutdownPeriod.toMillis(), TimeUnit.MILLISECONDS) + .whenComplete((unused, throwable) -> { + this.queueToConsumers.clear(); + callback.run(); + }); + } + finally { + this.lock.unlock(); + } + } + + /** + * Pause all the consumer for all queues. + */ + public void pause() { + this.queueToConsumers.values() + .stream() + .flatMap(List::stream) + .forEach(Consumer::pause); + } + + /** + * Resume all the consumer for all queues. + */ + public void resume() { + this.queueToConsumers.values() + .stream() + .flatMap(List::stream) + .forEach(Consumer::unpause); + } + + /** + * Pause all the consumer for specific queue. + */ + public void pause(String queueName) { + List consumers = this.queueToConsumers.get(queueName); + if (consumers != null) { + consumers.forEach(Consumer::pause); + } + } + + /** + * Resume all the consumer for specific queue. + */ + public void resume(String queueName) { + List consumers = this.queueToConsumers.get(queueName); + if (consumers != null) { + consumers.forEach(Consumer::unpause); + } + } + +} diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListener.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListener.java new file mode 100644 index 0000000000..70cc52cbf6 --- /dev/null +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListener.java @@ -0,0 +1,42 @@ +/* + * Copyright 2021-2025 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.rabbitmq.client.listener; + +import com.rabbitmq.client.amqp.Consumer; +import com.rabbitmq.client.amqp.Message; +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.core.MessageListener; + +/** + * A message listener that receives native AMQP 1.0 messages from RabbitMQ. + * + * @author Artem Bilan + * + * @since 4.0 + */ +public interface RabbitAmqpMessageListener extends MessageListener { + + /** + * Process an AMQP message. + * @param message the message to process. + * @param context the consumer context to settle message. + * Null if container is configured for {@code autoSettle}. + */ + void onAmqpMessage(Message message, Consumer.@Nullable Context context); + +} diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListenerAdapter.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListenerAdapter.java new file mode 100644 index 0000000000..98ba024e79 --- /dev/null +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListenerAdapter.java @@ -0,0 +1,72 @@ +/* + * Copyright 2025 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.rabbitmq.client.listener; + +import java.lang.reflect.Method; + +import com.rabbitmq.client.amqp.Consumer; +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.rabbit.listener.adapter.InvocationResult; +import org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter; +import org.springframework.amqp.rabbit.listener.api.RabbitListenerErrorHandler; +import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpUtils; + +/** + * A {@link MessagingMessageListenerAdapter} extension for the {@link RabbitAmqpMessageListener}. + * Provides these arguments for the {@link #getHandlerAdapter()} invocation: + *
    + *
  • {@link com.rabbitmq.client.amqp.Message} - the native AMQP 1.0 message without any conversions
  • + *
  • {@link org.springframework.amqp.core.Message} - Spring AMQP message abstraction as conversion result from the native AMQP 1.0 message
  • + *
  • {@link org.springframework.messaging.Message} - Spring Messaging abstraction as conversion result from the Spring AMQP message
  • + *
  • {@link Consumer.Context} - RabbitMQ AMQP client consumer settlement API.
  • + *
  • {@link org.springframework.amqp.core.AmqpAcknowledgment} - Spring AMQP acknowledgment abstraction: delegates to the {@link Consumer.Context}
  • + *
+ * + * @author Artem Bilan + * + * @since 4.0 + */ +public class RabbitAmqpMessageListenerAdapter extends MessagingMessageListenerAdapter + implements RabbitAmqpMessageListener { + + public RabbitAmqpMessageListenerAdapter(@Nullable Object bean, @Nullable Method method, boolean returnExceptions, + @Nullable RabbitListenerErrorHandler errorHandler) { + + super(bean, method, returnExceptions, errorHandler); + } + + @Override + public void onAmqpMessage(com.rabbitmq.client.amqp.Message amqpMessage, Consumer.@Nullable Context context) { + try { + org.springframework.amqp.core.Message springMessage = RabbitAmqpUtils.fromAmqpMessage(amqpMessage, context); + org.springframework.messaging.Message messagingMessage = toMessagingMessage(springMessage); + InvocationResult result = getHandlerAdapter() + .invoke(messagingMessage, + springMessage, springMessage.getMessageProperties().getAmqpAcknowledgment(), + amqpMessage, context); + if (result.getReturnValue() != null) { + logger.warn("Replies are not currently supported with RabbitMQ AMQP 1.0 listeners"); + } + } + catch (Exception ex) { + throw new ListenerExecutionFailedException("Failed to invoke listener", ex); + } + } + +} diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/package-info.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/package-info.java new file mode 100644 index 0000000000..23d84893c6 --- /dev/null +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/package-info.java @@ -0,0 +1,5 @@ +/** + * Provides Spring support for RabbitMQ AMQP 1.0 Consumer. + */ +@org.jspecify.annotations.NullMarked +package org.springframework.amqp.rabbitmq.client.listener; \ No newline at end of file From 6402004702830bc913b945137dd2e1373eaae5a9 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 3 Mar 2025 15:16:58 -0500 Subject: [PATCH 700/737] GH-2994: Add tests for `RabbitAmqpListenerContainer` Fixes: https://github.com/spring-projects/spring-amqp/issues/2994 * Add `@RabbitListener` tests for `RabbitAmqpMessageListener` * Adjust `RabbitAmqpMessageListener` error handling logic to call `errorHandler` first * Move `consumer.pause()` for `RabbitAmqpMessageListener.stop()` to the `supplyAsync()` to initiate pause for all consumers in parallel * Expose a failed `message` to the `ListenerExecutionFailedException` from the `RabbitAmqpMessageListenerAdapter` * Ensure RabbitMQ objects are deleted in the end of tests --- .../amqp/rabbitmq/client/RabbitAmqpAdmin.java | 4 +- .../listener/RabbitAmqpListenerContainer.java | 33 ++-- .../RabbitAmqpMessageListenerAdapter.java | 4 +- .../rabbitmq/client/RabbitAmqpAdminTests.java | 25 +-- .../client/RabbitAmqpTemplateTests.java | 4 +- .../rabbitmq/client/RabbitAmqpTestBase.java | 50 +++++- .../listener/RabbitAmqpListenerTests.java | 163 ++++++++++++++++++ 7 files changed, 242 insertions(+), 41 deletions(-) create mode 100644 spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdmin.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdmin.java index 226a3413a7..8394f34ea1 100644 --- a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdmin.java +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdmin.java @@ -176,13 +176,13 @@ public boolean isRunning() { */ @Override public void initialize() { - redeclareBeanDeclarables(); + declareDeclarableBeans(); } /** * Process bean declarables. */ - private void redeclareBeanDeclarables() { + private void declareDeclarableBeans() { if (this.applicationContext == null) { LOG.debug("no ApplicationContext has been set, cannot auto-declare Exchanges, Queues, and Bindings"); return; diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerContainer.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerContainer.java index 479af40f89..034dd85aa9 100644 --- a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerContainer.java +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerContainer.java @@ -256,24 +256,25 @@ private void invokeListener(Consumer.Context context, com.rabbitmq.client.amqp.M } } catch (Exception ex) { - if (!handleSpecialErrors(ex, context)) { - try { - this.errorHandler.handleError(ex); - // If error handler does not re-throw an exception, treat it as a successful processing result. + try { + this.errorHandler.handleError(ex); + // If error handler does not re-throw an exception, re-check original error. + // If it is not special, treat the error handler outcome as a successful processing result. + if (!handleSpecialErrors(ex, context)) { context.accept(); } - catch (Exception rethrow) { - if (!handleSpecialErrors(rethrow, context)) { - if (this.defaultRequeue) { - context.requeue(); - } - else { - context.discard(); - } - LOG.error(rethrow, () -> - "The 'errorHandler' has thrown an exception. The '" + amqpMessage + "' is " - + (this.defaultRequeue ? "re-queued." : "discarded.")); + } + catch (Exception rethrow) { + if (!handleSpecialErrors(rethrow, context)) { + if (this.defaultRequeue) { + context.requeue(); + } + else { + context.discard(); } + LOG.error(rethrow, () -> + "The 'errorHandler' has thrown an exception. The '" + amqpMessage + "' is " + + (this.defaultRequeue ? "re-queued." : "discarded.")); } } } @@ -321,9 +322,9 @@ public void stop(Runnable callback) { CompletableFuture[] completableFutures = this.queueToConsumers.values().stream() .flatMap(List::stream) - .peek(Consumer::pause) .map((consumer) -> CompletableFuture.supplyAsync(() -> { + consumer.pause(); try (consumer) { while (consumer.unsettledMessageCount() > 0) { Thread.sleep(100); diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListenerAdapter.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListenerAdapter.java index 98ba024e79..d52f0d03b9 100644 --- a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListenerAdapter.java +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListenerAdapter.java @@ -53,8 +53,8 @@ public RabbitAmqpMessageListenerAdapter(@Nullable Object bean, @Nullable Method @Override public void onAmqpMessage(com.rabbitmq.client.amqp.Message amqpMessage, Consumer.@Nullable Context context) { + org.springframework.amqp.core.Message springMessage = RabbitAmqpUtils.fromAmqpMessage(amqpMessage, context); try { - org.springframework.amqp.core.Message springMessage = RabbitAmqpUtils.fromAmqpMessage(amqpMessage, context); org.springframework.messaging.Message messagingMessage = toMessagingMessage(springMessage); InvocationResult result = getHandlerAdapter() .invoke(messagingMessage, @@ -65,7 +65,7 @@ public void onAmqpMessage(com.rabbitmq.client.amqp.Message amqpMessage, Consumer } } catch (Exception ex) { - throw new ListenerExecutionFailedException("Failed to invoke listener", ex); + throw new ListenerExecutionFailedException("Failed to invoke listener", ex, springMessage); } } diff --git a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdminTests.java b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdminTests.java index 8a95bb0233..22b6561b41 100644 --- a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdminTests.java +++ b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdminTests.java @@ -64,15 +64,6 @@ void verifyBeanDeclarations() { assertThat(template.receiveAndConvert("q3")).succeedsWithin(Duration.ofSeconds(10)).isEqualTo("test4"); assertThat(template.receiveAndConvert("q4")).succeedsWithin(Duration.ofSeconds(10)).isEqualTo("test5"); - admin.deleteQueue("q1"); - admin.deleteQueue("q2"); - admin.deleteQueue("q3"); - admin.deleteQueue("q4"); - admin.deleteExchange("e1"); - admin.deleteExchange("e2"); - admin.deleteExchange("e3"); - admin.deleteExchange("e4"); - assertThat(declarables.getDeclarablesByType(Queue.class)) .hasSize(1) .extracting(Queue::getName) @@ -92,12 +83,12 @@ public static class Config { @Bean DirectExchange e1() { - return new DirectExchange("e1", false, false); + return new DirectExchange("e1"); } @Bean Queue q1() { - return new Queue("q1", false, false, false); + return new Queue("q1"); } @Bean @@ -108,15 +99,15 @@ Binding b1() { @Bean Declarables es() { return new Declarables( - new DirectExchange("e2", false, false), - new DirectExchange("e3", false, false)); + new DirectExchange("e2"), + new DirectExchange("e3")); } @Bean Declarables qs() { return new Declarables( - new Queue("q2", false, false, false), - new Queue("q3", false, false, false)); + new Queue("q2"), + new Queue("q3")); } @Bean @@ -129,8 +120,8 @@ Declarables bs() { @Bean Declarables ds() { return new Declarables( - new DirectExchange("e4", false, false), - new Queue("q4", false, false, false), + new DirectExchange("e4"), + new Queue("q4"), new Binding("q4", Binding.DestinationType.QUEUE, "e4", "k4", null)); } diff --git a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplateTests.java b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplateTests.java index bff14523f6..c4dc11460d 100644 --- a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplateTests.java +++ b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplateTests.java @@ -102,12 +102,12 @@ static class Config { @Bean DirectExchange e1() { - return new DirectExchange("e1", false, false); + return new DirectExchange("e1"); } @Bean Queue q1() { - return new Queue("q1", false, false, false); + return new Queue("q1"); } @Bean diff --git a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTestBase.java b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTestBase.java index 4d7284126c..cdd8cc0392 100644 --- a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTestBase.java +++ b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTestBase.java @@ -16,12 +16,21 @@ package org.springframework.amqp.rabbitmq.client; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + import com.rabbitmq.client.amqp.Connection; import com.rabbitmq.client.amqp.Environment; import com.rabbitmq.client.amqp.impl.AmqpEnvironmentBuilder; +import org.springframework.amqp.core.Declarable; +import org.springframework.amqp.core.Declarables; +import org.springframework.amqp.core.Exchange; +import org.springframework.amqp.core.Queue; import org.springframework.amqp.rabbit.junit.AbstractTestContainerTests; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.Lifecycle; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.test.annotation.DirtiesContext; @@ -36,7 +45,7 @@ */ @SpringJUnitConfig @DirtiesContext -abstract class RabbitAmqpTestBase extends AbstractTestContainerTests { +public abstract class RabbitAmqpTestBase extends AbstractTestContainerTests { @Autowired protected Environment environment; @@ -51,7 +60,16 @@ abstract class RabbitAmqpTestBase extends AbstractTestContainerTests { protected RabbitAmqpTemplate template; @Configuration - public static class AmqpCommonConfig { + public static class AmqpCommonConfig implements Lifecycle { + + @Autowired + List declarables; + + @Autowired(required = false) + List declarableContainers = new ArrayList<>(); + + @Autowired + RabbitAmqpAdmin admin; @Bean Environment environment() { @@ -77,6 +95,34 @@ RabbitAmqpTemplate rabbitTemplate(Connection connection) { return new RabbitAmqpTemplate(connection); } + volatile boolean running; + + @Override + public void start() { + this.running = true; + } + + @Override + public boolean isRunning() { + return this.running; + } + + @Override + public void stop() { + Stream.concat(this.declarables.stream(), + this.declarableContainers.stream() + .flatMap((declarables) -> declarables.getDeclarables().stream())) + .filter((declarable) -> declarable instanceof Queue || declarable instanceof Exchange) + .forEach((declarable) -> { + if (declarable instanceof Queue queue) { + this.admin.deleteQueue(queue.getName()); + } + else { + this.admin.deleteExchange(((Exchange) declarable).getName()); + } + }); + } + } } diff --git a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java new file mode 100644 index 0000000000..55d83b2959 --- /dev/null +++ b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java @@ -0,0 +1,163 @@ +/* + * Copyright 2025 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.rabbitmq.client.listener; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import com.rabbitmq.client.amqp.Connection; +import com.rabbitmq.client.amqp.Consumer; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.core.AmqpAcknowledgment; +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.QueueBuilder; +import org.springframework.amqp.core.TopicExchange; +import org.springframework.amqp.rabbit.annotation.EnableRabbit; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.annotation.RabbitListenerAnnotationBeanPostProcessor; +import org.springframework.amqp.rabbit.listener.MessageListenerContainer; +import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpTestBase; +import org.springframework.amqp.rabbitmq.client.config.RabbitAmqpListenerContainerFactory; +import org.springframework.amqp.support.converter.MessageConversionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Artem Bilan + * + * @since 4.0 + */ +@ContextConfiguration +class RabbitAmqpListenerTests extends RabbitAmqpTestBase { + + @Autowired + Config config; + + @Autowired + RabbitListenerEndpointRegistry rabbitListenerEndpointRegistry; + + @Test + @SuppressWarnings("unchecked") + void verifyAllDataIsConsumedFromQ1AndQ2() throws InterruptedException { + MessageListenerContainer testAmqpListener = + this.rabbitListenerEndpointRegistry.getListenerContainer("testAmqpListener"); + + assertThat(testAmqpListener).extracting("queueToConsumers") + .asInstanceOf(InstanceOfAssertFactories.map(String.class, List.class)) + .hasSize(2) + .values() + .flatMap(list -> (List) list) + .hasSize(4); + + List testDataList = + List.of("data1", "data2", "requeue", "data4", "data5", "discard", "data7", "data8", "discard", "data10"); + + Random random = new Random(); + + for (String testData : testDataList) { + this.template.convertAndSend((random.nextInt(2) == 0 ? "q1" : "q2"), testData); + } + + assertThat(this.config.consumeIsDone.await(10, TimeUnit.SECONDS)).isTrue(); + + assertThat(this.config.received).containsAll(testDataList); + + assertThat(this.template.receive("dlq1")).succeedsWithin(10, TimeUnit.SECONDS); + assertThat(this.template.receive("dlq1")).succeedsWithin(10, TimeUnit.SECONDS); + } + + @Configuration + @EnableRabbit + static class Config { + + @Bean + TopicExchange dlx1() { + return new TopicExchange("dlx1"); + } + + @Bean + Queue dlq1() { + return new Queue("dlq1"); + } + + @Bean + Binding dlq1Binding() { + return BindingBuilder.bind(dlq1()).to(dlx1()).with("#"); + } + + @Bean + Queue q1() { + return QueueBuilder.durable("q1").deadLetterExchange("dlx1").build(); + } + + @Bean + Queue q2() { + return QueueBuilder.durable("q2").deadLetterExchange("dlx1").build(); + } + + @Bean(RabbitListenerAnnotationBeanPostProcessor.DEFAULT_RABBIT_LISTENER_CONTAINER_FACTORY_BEAN_NAME) + RabbitAmqpListenerContainerFactory rabbitAmqpListenerContainerFactory(Connection connection) { + return new RabbitAmqpListenerContainerFactory(connection); + } + + List received = Collections.synchronizedList(new ArrayList<>()); + + CountDownLatch consumeIsDone = new CountDownLatch(10); + + @RabbitListener(queues = {"q1", "q2"}, + ackMode = "#{T(org.springframework.amqp.core.AcknowledgeMode).MANUAL}", + concurrency = "2", + id = "testAmqpListener") + void processQ1AndQ2Data(String data, AmqpAcknowledgment acknowledgment, Consumer.Context context) { + try { + if ("discard".equals(data)) { + if (!this.received.contains(data)) { + context.discard(); + } + else { + throw new MessageConversionException("Test message is rejected"); + } + } + else if ("requeue".equals(data) && !this.received.contains(data)) { + acknowledgment.acknowledge(AmqpAcknowledgment.Status.REQUEUE); + } + else { + acknowledgment.acknowledge(); + } + this.received.add(data); + } + finally { + this.consumeIsDone.countDown(); + } + } + + } + +} From 3d5a6a9766a08eb502ee552f7b723c82ff5c0ae7 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 4 Mar 2025 14:20:58 -0500 Subject: [PATCH 701/737] GH-3001: Add consume batch support to `RabbitAmqpListenerContainer` Fixes: https://github.com/spring-projects/spring-amqp/issues/3001 * Expose batch related options for `RabbitAmqpListenerContainer` and `RabbitAmqpListenerContainerFactory`, respectively * `batchSize` - the indicator that `RabbitAmqpListenerContainer` (and `RabbitAmqpMessageListenerAdapter`) has to work in batch mode * `batchReceiveTimeout` - how long to wait for batch to be fulfilled or release whatever was gathered so far, even if just only one message * `taskScheduler` - schedule "force batch release" after `batchReceiveTimeout` * Make `MessagingMessageListenerAdapter` `final` properties as `protected` to avoid undesired copy-paste burden --- .../BatchMessagingMessageListenerAdapter.java | 13 +- .../MessagingMessageListenerAdapter.java | 12 +- .../RabbitAmqpListenerContainerFactory.java | 73 +++++- .../listener/RabbitAmqpListenerContainer.java | 234 ++++++++++++++++-- .../RabbitAmqpMessageListenerAdapter.java | 64 ++++- .../listener/RabbitAmqpListenerTests.java | 89 +++++++ 6 files changed, 437 insertions(+), 48 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapter.java index ecb97f6016..144d064b53 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/BatchMessagingMessageListenerAdapter.java @@ -49,8 +49,6 @@ public class BatchMessagingMessageListenerAdapter extends MessagingMessageListenerAdapter implements ChannelAwareBatchMessageListener { - private final MessagingMessageConverterAdapter converterAdapter; - private final BatchingStrategy batchingStrategy; @SuppressWarnings("this-escape") @@ -58,14 +56,13 @@ public BatchMessagingMessageListenerAdapter(@Nullable Object bean, @Nullable Met @Nullable RabbitListenerErrorHandler errorHandler, @Nullable BatchingStrategy batchingStrategy) { super(bean, method, returnExceptions, errorHandler, true); - this.converterAdapter = (MessagingMessageConverterAdapter) getMessagingMessageConverter(); this.batchingStrategy = batchingStrategy == null ? new SimpleBatchingStrategy(0, 0, 0L) : batchingStrategy; } @Override public void onMessageBatch(List messages, @Nullable Channel channel) { Message converted; - if (this.converterAdapter.isAmqpMessageList()) { + if (this.messagingMessageConverter.isAmqpMessageList()) { converted = new GenericMessage<>(messages); } else { @@ -87,7 +84,7 @@ public void onMessageBatch(List messages, } } } - if (this.converterAdapter.isMessageList()) { + if (this.messagingMessageConverter.isMessageList()) { converted = new GenericMessage<>(messagingMessages); } else { @@ -178,7 +175,7 @@ private void asyncFailure(List requests, protected Message toMessagingMessage(org.springframework.amqp.core.Message amqpMessage) { if (this.batchingStrategy.canDebatch(amqpMessage.getMessageProperties())) { - if (this.converterAdapter.isMessageList()) { + if (this.messagingMessageConverter.isMessageList()) { List> messages = new ArrayList<>(); this.batchingStrategy.deBatch(amqpMessage, fragment -> messages.add(super.toMessagingMessage(fragment))); return new GenericMessage<>(messages); @@ -186,9 +183,9 @@ protected Message toMessagingMessage(org.springframework.amqp.core.Message am else { List list = new ArrayList<>(); this.batchingStrategy.deBatch(amqpMessage, fragment -> - list.add(this.converterAdapter.extractPayload(fragment))); + list.add(this.messagingMessageConverter.extractPayload(fragment))); return MessageBuilder.withPayload(list) - .copyHeaders(this.converterAdapter + .copyHeaders(this.messagingMessageConverter .getHeaderMapper() .toHeaders(amqpMessage.getMessageProperties())) .build(); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java index b6033b14bf..a0a99834a4 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java @@ -69,11 +69,11 @@ */ public class MessagingMessageListenerAdapter extends AbstractAdaptableMessageListener { - private final MessagingMessageConverterAdapter messagingMessageConverter; + protected final MessagingMessageConverterAdapter messagingMessageConverter; - private final boolean returnExceptions; + protected final boolean returnExceptions; - private final @Nullable RabbitListenerErrorHandler errorHandler; + protected final @Nullable RabbitListenerErrorHandler errorHandler; private @Nullable HandlerAdapter handlerAdapter; @@ -364,15 +364,15 @@ protected final class MessagingMessageConverterAdapter extends MessagingMessageC } } - protected boolean isMessageList() { + public boolean isMessageList() { return this.isMessageList; } - protected boolean isAmqpMessageList() { + public boolean isAmqpMessageList() { return this.isAmqpMessageList; } - protected @Nullable Method getMethod() { + public @Nullable Method getMethod() { return this.method; } diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/config/RabbitAmqpListenerContainerFactory.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/config/RabbitAmqpListenerContainerFactory.java index 926580f5a0..6195a2167d 100644 --- a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/config/RabbitAmqpListenerContainerFactory.java +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/config/RabbitAmqpListenerContainerFactory.java @@ -16,10 +16,13 @@ package org.springframework.amqp.rabbitmq.client.config; +import java.util.Arrays; + import com.rabbitmq.client.amqp.Connection; -import org.aopalliance.aop.Advice; import org.jspecify.annotations.Nullable; +import org.springframework.amqp.core.MessageListener; +import org.springframework.amqp.core.MessagePostProcessor; import org.springframework.amqp.rabbit.config.BaseRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.config.ContainerCustomizer; import org.springframework.amqp.rabbit.listener.MethodRabbitListenerEndpoint; @@ -27,6 +30,7 @@ import org.springframework.amqp.rabbitmq.client.listener.RabbitAmqpListenerContainer; import org.springframework.amqp.rabbitmq.client.listener.RabbitAmqpMessageListenerAdapter; import org.springframework.amqp.utils.JavaUtils; +import org.springframework.scheduling.TaskScheduler; /** * Factory for {@link RabbitAmqpListenerContainer}. @@ -45,6 +49,14 @@ public class RabbitAmqpListenerContainerFactory private @Nullable ContainerCustomizer containerCustomizer; + private MessagePostProcessor @Nullable [] afterReceivePostProcessors; + + private @Nullable Integer batchSize; + + private @Nullable Long batchReceiveTimeout; + + private @Nullable TaskScheduler taskScheduler; + /** * Construct an instance using the provided amqpConnection. * @param amqpConnection the connection. @@ -62,18 +74,69 @@ public void setContainerCustomizer(ContainerCustomizer 1}) which turns the target listener container into a batch mode. + * @param batchSize the batch size. + * @see RabbitAmqpListenerContainer#setBatchSize + * @see #setBatchReceiveTimeout(Long) + */ + public void setBatchSize(Integer batchSize) { + this.batchSize = batchSize; + } + + /** + * The number of milliseconds of timeout for gathering batch messages. + * It limits the time to wait to fill batchSize. + * Default is 30 seconds. + * @param batchReceiveTimeout the timeout for gathering batch messages. + * @see RabbitAmqpListenerContainer#setBatchReceiveTimeout + * @see #setBatchSize(Integer) + */ + public void setBatchReceiveTimeout(Long batchReceiveTimeout) { + this.batchReceiveTimeout = batchReceiveTimeout; + } + + /** + * Configure a {@link TaskScheduler} to release not fulfilled batches after timeout. + * @param taskScheduler the {@link TaskScheduler} to use. + * @see RabbitAmqpListenerContainer#setTaskScheduler(TaskScheduler) + * @see #setBatchReceiveTimeout(Long) + */ + public void setTaskScheduler(TaskScheduler taskScheduler) { + this.taskScheduler = taskScheduler; + } + @Override public RabbitAmqpListenerContainer createListenerContainer(@Nullable RabbitListenerEndpoint endpoint) { if (endpoint instanceof MethodRabbitListenerEndpoint methodRabbitListenerEndpoint) { + JavaUtils.INSTANCE + .acceptIfCondition(this.batchSize != null && this.batchSize > 1, + true, + methodRabbitListenerEndpoint::setBatchListener); + methodRabbitListenerEndpoint.setAdapterProvider( (batch, bean, method, returnExceptions, errorHandler, batchingStrategy) -> - new RabbitAmqpMessageListenerAdapter(bean, method, returnExceptions, errorHandler)); + new RabbitAmqpMessageListenerAdapter(bean, method, returnExceptions, errorHandler, batch)); } RabbitAmqpListenerContainer container = createContainerInstance(); - Advice[] adviceChain = getAdviceChain(); JavaUtils.INSTANCE - .acceptIfNotNull(adviceChain, container::setAdviceChain) - .acceptIfNotNull(getDefaultRequeueRejected(), container::setDefaultRequeue); + .acceptIfNotNull(getAdviceChain(), container::setAdviceChain) + .acceptIfNotNull(getDefaultRequeueRejected(), container::setDefaultRequeue) + .acceptIfNotNull(this.afterReceivePostProcessors, container::setAfterReceivePostProcessors) + .acceptIfNotNull(this.batchSize, container::setBatchSize) + .acceptIfNotNull(this.batchReceiveTimeout, container::setBatchReceiveTimeout) + .acceptIfNotNull(this.taskScheduler, container::setTaskScheduler); applyCommonOverrides(endpoint, container); diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerContainer.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerContainer.java index 034dd85aa9..eac1699933 100644 --- a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerContainer.java +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerContainer.java @@ -17,9 +17,14 @@ package org.springframework.amqp.rabbitmq.client.listener; import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; +import java.util.Objects; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -36,13 +41,19 @@ import org.springframework.amqp.core.AmqpAcknowledgment; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageListener; +import org.springframework.amqp.core.MessagePostProcessor; import org.springframework.amqp.rabbit.listener.ConditionalRejectingErrorHandler; import org.springframework.amqp.rabbit.listener.MessageListenerContainer; import org.springframework.amqp.rabbit.listener.support.ContainerUtils; import org.springframework.amqp.rabbitmq.client.RabbitAmqpUtils; +import org.springframework.amqp.support.postprocessor.MessagePostProcessorUtils; import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; import org.springframework.core.log.LogAccessor; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.util.Assert; import org.springframework.util.ErrorHandler; import org.springframework.util.LinkedMultiValueMap; @@ -57,7 +68,7 @@ * @since 4.0 * */ -public class RabbitAmqpListenerContainer implements MessageListenerContainer { +public class RabbitAmqpListenerContainer implements MessageListenerContainer, BeanNameAware, DisposableBean { private static final LogAccessor LOG = new LogAccessor(LogFactory.getLog(RabbitAmqpListenerContainer.class)); @@ -85,14 +96,28 @@ public class RabbitAmqpListenerContainer implements MessageListenerContainer { private @Nullable MessageListener messageListener; + private @Nullable MessageListener proxy; + private ErrorHandler errorHandler = new ConditionalRejectingErrorHandler(); + private @Nullable Collection afterReceivePostProcessors; + private boolean autoStartup = true; + private String beanName = "not.a.Spring.bean"; + private @Nullable String listenerId; private Duration gracefulShutdownPeriod = Duration.ofSeconds(30); + private int batchSize; + + private Duration batchReceiveDuration = Duration.ofSeconds(30); + + private @Nullable TaskScheduler taskScheduler; + + private boolean internalTaskScheduler = true; + /** * Construct an instance using the provided connection. * @param connection to use. @@ -118,6 +143,39 @@ public void setStateListeners(Resource.StateListener... stateListeners) { this.stateListeners = Arrays.copyOf(stateListeners, stateListeners.length); } + /** + * Set {@link MessagePostProcessor}s that will be applied after message reception, before + * invoking the {@link MessageListener}. Often used to decompress data. Processors are invoked in order, + * depending on {@code PriorityOrder}, {@code Order} and finally unordered. + * @param afterReceivePostProcessors the post processor. + */ + public void setAfterReceivePostProcessors(MessagePostProcessor... afterReceivePostProcessors) { + this.afterReceivePostProcessors = MessagePostProcessorUtils.sort(Arrays.asList(afterReceivePostProcessors)); + } + + public void setBatchSize(int batchSize) { + Assert.isTrue(batchSize > 1, "'batchSize' must be greater than 1"); + this.batchSize = batchSize; + } + + public void setBatchReceiveTimeout(long batchReceiveTimeout) { + this.batchReceiveDuration = Duration.ofMillis(batchReceiveTimeout); + } + + /** + * Set a {@link TaskScheduler} for monitoring batch releases. + * @param taskScheduler the {@link TaskScheduler} to use. + */ + public void setTaskScheduler(TaskScheduler taskScheduler) { + this.taskScheduler = taskScheduler; + this.internalTaskScheduler = false; + } + + @Override + public void setBeanName(String name) { + this.beanName = name; + } + @Override public void setAutoStartup(boolean autoStart) { this.autoStartup = autoStart; @@ -185,22 +243,31 @@ public void setListenerId(String id) { this.listenerId = id; } + /** + * The 'id' attribute of the listener. + * @return the id (or the container bean name if no id set). + */ + public String getListenerId() { + return this.listenerId != null ? this.listenerId : this.beanName; + } + @Override public void setupMessageListener(MessageListener messageListener) { this.messageListener = messageListener; + this.proxy = this.messageListener; if (!ObjectUtils.isEmpty(this.adviceChain)) { ProxyFactory factory = new ProxyFactory(messageListener); for (Advice advice : this.adviceChain) { factory.addAdvisor(new DefaultPointcutAdvisor(advice)); } factory.setInterfaces(messageListener.getClass().getInterfaces()); - this.messageListener = (MessageListener) factory.getProxy(getClass().getClassLoader()); + this.proxy = (MessageListener) factory.getProxy(getClass().getClassLoader()); } } @Override public @Nullable Object getMessageListener() { - return this.messageListener; + return this.proxy; } @Override @@ -209,6 +276,18 @@ public void afterPropertiesSet() { Assert.state(this.messageListener != null, "The 'messageListener' must be provided."); this.messageListener.containerAckMode(this.autoSettle ? AcknowledgeMode.AUTO : AcknowledgeMode.MANUAL); + if (this.messageListener instanceof RabbitAmqpMessageListenerAdapter adapter + && this.afterReceivePostProcessors != null) { + + adapter.setAfterReceivePostProcessors(this.afterReceivePostProcessors); + } + + if (this.batchSize > 1 && this.internalTaskScheduler) { + ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); + threadPoolTaskScheduler.setThreadNamePrefix(getListenerId() + "-consumerMonitor-"); + threadPoolTaskScheduler.afterPropertiesSet(); + this.taskScheduler = threadPoolTaskScheduler; + } } @Override @@ -236,7 +315,7 @@ public void start() { .priority(this.priority) .initialCredits(this.initialCredits) .listeners(this.stateListeners) - .messageHandler(this::invokeListener) + .messageHandler(new ConsumerMessageHandler()) .build(); this.queueToConsumers.add(queue, consumer); } @@ -256,39 +335,65 @@ private void invokeListener(Consumer.Context context, com.rabbitmq.client.amqp.M } } catch (Exception ex) { - try { - this.errorHandler.handleError(ex); - // If error handler does not re-throw an exception, re-check original error. - // If it is not special, treat the error handler outcome as a successful processing result. - if (!handleSpecialErrors(ex, context)) { - context.accept(); - } - } - catch (Exception rethrow) { - if (!handleSpecialErrors(rethrow, context)) { - if (this.defaultRequeue) { - context.requeue(); - } - else { - context.discard(); - } - LOG.error(rethrow, () -> - "The 'errorHandler' has thrown an exception. The '" + amqpMessage + "' is " - + (this.defaultRequeue ? "re-queued." : "discarded.")); - } - } + handleListenerError(ex, context, amqpMessage); } } @SuppressWarnings("NullAway") // Dataflow analysis limitation private void doInvokeListener(Consumer.Context context, com.rabbitmq.client.amqp.Message amqpMessage) { Consumer.@Nullable Context contextToUse = this.autoSettle ? null : context; - if (this.messageListener instanceof RabbitAmqpMessageListener amqpMessageListener) { + if (this.proxy instanceof RabbitAmqpMessageListener amqpMessageListener) { amqpMessageListener.onAmqpMessage(amqpMessage, contextToUse); } else { Message message = RabbitAmqpUtils.fromAmqpMessage(amqpMessage, contextToUse); - this.messageListener.onMessage(message); + this.proxy.onMessage(message); + } + } + + private void invokeBatchListener(Consumer.Context context, List batch) { + Consumer.@Nullable Context contextToUse = this.autoSettle ? null : context; + List messages = + batch.stream() + .map((amqpMessage) -> RabbitAmqpUtils.fromAmqpMessage(amqpMessage, contextToUse)) + .toList(); + try { + doInvokeBatchListener(messages); + if (this.autoSettle) { + context.accept(); + } + } + catch (Exception ex) { + handleListenerError(ex, context, batch); + } + } + + @SuppressWarnings("NullAway") // Dataflow analysis limitation + private void doInvokeBatchListener(List messages) { + this.proxy.onMessageBatch(messages); + } + + private void handleListenerError(Exception ex, Consumer.Context context, Object messageOrBatch) { + try { + this.errorHandler.handleError(ex); + // If error handler does not re-throw an exception, re-check original error. + // If it is not special, treat the error handler outcome as a successful processing result. + if (!handleSpecialErrors(ex, context)) { + context.accept(); + } + } + catch (Exception rethrow) { + if (!handleSpecialErrors(rethrow, context)) { + if (this.defaultRequeue) { + context.requeue(); + } + else { + context.discard(); + } + LOG.error(rethrow, () -> + "The 'errorHandler' has thrown an exception. The '" + messageOrBatch + "' is " + + (this.defaultRequeue ? "re-queued." : "discarded.")); + } } } @@ -390,4 +495,79 @@ public void resume(String queueName) { } } + @Override + public void destroy() { + if (this.internalTaskScheduler && this.taskScheduler != null) { + ((ThreadPoolTaskScheduler) this.taskScheduler).shutdown(); + } + } + + private class ConsumerMessageHandler implements Consumer.MessageHandler { + + private volatile @Nullable ConsumerBatch consumerBatch; + + ConsumerMessageHandler() { + } + + @Override + public void handle(Consumer.Context context, com.rabbitmq.client.amqp.Message message) { + if (RabbitAmqpListenerContainer.this.batchSize > 1) { + ConsumerBatch currentBatch = this.consumerBatch; + if (currentBatch == null || currentBatch.batchReleaseFuture == null) { + currentBatch = new ConsumerBatch(context.batch(RabbitAmqpListenerContainer.this.batchSize)); + this.consumerBatch = currentBatch; + } + currentBatch.add(context, message); + if (currentBatch.batchContext.size() == RabbitAmqpListenerContainer.this.batchSize) { + currentBatch.release(); + this.consumerBatch = null; + } + } + else { + invokeListener(context, message); + } + } + + private class ConsumerBatch { + + private final List batch = new ArrayList<>(); + + private final Consumer.BatchContext batchContext; + + private volatile @Nullable ScheduledFuture batchReleaseFuture; + + ConsumerBatch(Consumer.BatchContext batchContext) { + this.batchContext = batchContext; + } + + void add(Consumer.Context context, com.rabbitmq.client.amqp.Message message) { + this.batchContext.add(context); + this.batch.add(message); + if (this.batchReleaseFuture == null) { + this.batchReleaseFuture = + Objects.requireNonNull(RabbitAmqpListenerContainer.this.taskScheduler) + .schedule(this::releaseInternal, + Instant.now().plus(RabbitAmqpListenerContainer.this.batchReceiveDuration)); + } + } + + void release() { + ScheduledFuture currentBatchReleaseFuture = this.batchReleaseFuture; + if (currentBatchReleaseFuture != null) { + currentBatchReleaseFuture.cancel(true); + releaseInternal(); + } + } + + private void releaseInternal() { + if (this.batchReleaseFuture != null) { + this.batchReleaseFuture = null; + invokeBatchListener(this.batchContext, this.batch); + } + } + + } + + } + } diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListenerAdapter.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListenerAdapter.java index d52f0d03b9..a94e2f88aa 100644 --- a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListenerAdapter.java +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListenerAdapter.java @@ -17,15 +17,22 @@ package org.springframework.amqp.rabbitmq.client.listener; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; import com.rabbitmq.client.amqp.Consumer; import org.jspecify.annotations.Nullable; +import org.springframework.amqp.core.AmqpAcknowledgment; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessagePostProcessor; import org.springframework.amqp.rabbit.listener.adapter.InvocationResult; import org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter; import org.springframework.amqp.rabbit.listener.api.RabbitListenerErrorHandler; import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; import org.springframework.amqp.rabbitmq.client.RabbitAmqpUtils; +import org.springframework.messaging.support.GenericMessage; /** * A {@link MessagingMessageListenerAdapter} extension for the {@link RabbitAmqpMessageListener}. @@ -45,15 +52,26 @@ public class RabbitAmqpMessageListenerAdapter extends MessagingMessageListenerAdapter implements RabbitAmqpMessageListener { + private @Nullable Collection afterReceivePostProcessors; + public RabbitAmqpMessageListenerAdapter(@Nullable Object bean, @Nullable Method method, boolean returnExceptions, - @Nullable RabbitListenerErrorHandler errorHandler) { + @Nullable RabbitListenerErrorHandler errorHandler, boolean batch) { + + super(bean, method, returnExceptions, errorHandler, batch); + } - super(bean, method, returnExceptions, errorHandler); + public void setAfterReceivePostProcessors(Collection afterReceivePostProcessors) { + this.afterReceivePostProcessors = new ArrayList<>(afterReceivePostProcessors); } @Override public void onAmqpMessage(com.rabbitmq.client.amqp.Message amqpMessage, Consumer.@Nullable Context context) { org.springframework.amqp.core.Message springMessage = RabbitAmqpUtils.fromAmqpMessage(amqpMessage, context); + if (this.afterReceivePostProcessors != null) { + for (MessagePostProcessor processor : this.afterReceivePostProcessors) { + springMessage = processor.postProcessMessage(springMessage); + } + } try { org.springframework.messaging.Message messagingMessage = toMessagingMessage(springMessage); InvocationResult result = getHandlerAdapter() @@ -69,4 +87,46 @@ public void onAmqpMessage(com.rabbitmq.client.amqp.Message amqpMessage, Consumer } } + @Override + public void onMessageBatch(List messages) { + AmqpAcknowledgment amqpAcknowledgment = + messages.stream() + .findAny() + .map((message) -> message.getMessageProperties().getAmqpAcknowledgment()) + .orElse(null); + + org.springframework.messaging.Message converted; + if (this.messagingMessageConverter.isAmqpMessageList()) { + converted = new GenericMessage<>(messages); + } + else { + List> messagingMessages = + messages.stream() + .map(this::toMessagingMessage) + .toList(); + + if (this.messagingMessageConverter.isMessageList()) { + converted = new GenericMessage<>(messagingMessages); + } + else { + List payloads = new ArrayList<>(); + for (org.springframework.messaging.Message message : messagingMessages) { + payloads.add(message.getPayload()); + } + converted = new GenericMessage<>(payloads); + } + } + try { + InvocationResult result = getHandlerAdapter() + .invoke(converted, amqpAcknowledgment); + if (result.getReturnValue() != null) { + logger.warn("Replies are not currently supported with RabbitMQ AMQP 1.0 listeners"); + } + } + catch (Exception ex) { + throw new ListenerExecutionFailedException("Failed to invoke listener", ex, + messages.toArray(new Message[0])); + } + } + } diff --git a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java index 55d83b2959..22af681df2 100644 --- a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java +++ b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java @@ -20,8 +20,10 @@ import java.util.Collections; import java.util.List; import java.util.Random; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; import com.rabbitmq.client.amqp.Connection; import com.rabbitmq.client.amqp.Consumer; @@ -42,10 +44,13 @@ import org.springframework.amqp.rabbitmq.client.RabbitAmqpTestBase; import org.springframework.amqp.rabbitmq.client.config.RabbitAmqpListenerContainerFactory; import org.springframework.amqp.support.converter.MessageConversionException; +import org.springframework.amqp.utils.test.TestUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.test.context.ContextConfiguration; +import org.springframework.util.MultiValueMap; import static org.assertj.core.api.Assertions.assertThat; @@ -93,6 +98,53 @@ void verifyAllDataIsConsumedFromQ1AndQ2() throws InterruptedException { assertThat(this.template.receive("dlq1")).succeedsWithin(10, TimeUnit.SECONDS); } + @Test + @SuppressWarnings("unchecked") + void verifyBatchConsumedAfterScheduledTimeout() { + List testDataList = + List.of("batchData1", "batchData2", "batchData3", "batchData4", "batchData5"); + + for (String testData : testDataList) { + this.template.convertAndSend("q3", testData); + } + + assertThat(this.config.batchReceived).succeedsWithin(10, TimeUnit.SECONDS) + .asInstanceOf(InstanceOfAssertFactories.LIST) + .hasSize(5) + .containsAll(testDataList); + + assertThat(this.config.batchReceivedOnThread).startsWith("batch-consumer-scheduler-"); + + MessageListenerContainer testBatchListener = + this.rabbitListenerEndpointRegistry.getListenerContainer("testBatchListener"); + + MultiValueMap queueToConsumers = + TestUtils.getPropertyValue(testBatchListener, "queueToConsumers", MultiValueMap.class); + Consumer consumer = queueToConsumers.get("q3").get(0); + + assertThat(consumer.unsettledMessageCount()).isEqualTo(0L); + + this.config.batchReceived = new CompletableFuture<>(); + + testDataList = + IntStream.range(6, 16) + .boxed() + .map(Object::toString) + .map("batchData"::concat) + .toList(); + + for (String testData : testDataList) { + this.template.convertAndSend("q3", testData); + } + + assertThat(this.config.batchReceived).succeedsWithin(10, TimeUnit.SECONDS) + .asInstanceOf(InstanceOfAssertFactories.LIST) + .hasSize(10) + .containsAll(testDataList); + + assertThat(this.config.batchReceivedOnThread).startsWith("dispatching-rabbitmq-amqp-"); + } + @Configuration @EnableRabbit static class Config { @@ -122,6 +174,11 @@ Queue q2() { return QueueBuilder.durable("q2").deadLetterExchange("dlx1").build(); } + @Bean + Queue q3() { + return new Queue("q3"); + } + @Bean(RabbitListenerAnnotationBeanPostProcessor.DEFAULT_RABBIT_LISTENER_CONTAINER_FACTORY_BEAN_NAME) RabbitAmqpListenerContainerFactory rabbitAmqpListenerContainerFactory(Connection connection) { return new RabbitAmqpListenerContainerFactory(connection); @@ -158,6 +215,38 @@ else if ("requeue".equals(data) && !this.received.contains(data)) { } } + @Bean + ThreadPoolTaskScheduler taskScheduler() { + ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); + threadPoolTaskScheduler.setPoolSize(2); + threadPoolTaskScheduler.setThreadNamePrefix("batch-consumer-scheduler-"); + return threadPoolTaskScheduler; + } + + @Bean + RabbitAmqpListenerContainerFactory batchRabbitAmqpListenerContainerFactory(Connection connection, + ThreadPoolTaskScheduler taskScheduler) { + + RabbitAmqpListenerContainerFactory rabbitAmqpListenerContainerFactory = + new RabbitAmqpListenerContainerFactory(connection); + rabbitAmqpListenerContainerFactory.setTaskScheduler(taskScheduler); + rabbitAmqpListenerContainerFactory.setBatchSize(10); + rabbitAmqpListenerContainerFactory.setBatchReceiveTimeout(1000L); + return rabbitAmqpListenerContainerFactory; + } + + CompletableFuture> batchReceived = new CompletableFuture<>(); + + volatile String batchReceivedOnThread; + + @RabbitListener(queues = "q3", + containerFactory = "batchRabbitAmqpListenerContainerFactory", + id = "testBatchListener") + void processBatchFromQ3(List data) { + this.batchReceivedOnThread = Thread.currentThread().getName(); + this.batchReceived.complete(data); + } + } } From 158243e76def75a2de37eabe0c24a62e4deba1df Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Mar 2025 14:22:42 -0500 Subject: [PATCH 702/737] Bump com.fasterxml.jackson:jackson-bom from 2.18.2 to 2.18.3 Bumps [com.fasterxml.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) from 2.18.2 to 2.18.3. - [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-2.18.2...jackson-bom-2.18.3) --- updated-dependencies: - dependency-name: com.fasterxml.jackson:jackson-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 3f06a8d37b..c2c2344c83 100644 --- a/build.gradle +++ b/build.gradle @@ -48,7 +48,7 @@ ext { commonsPoolVersion = '2.12.1' hamcrestVersion = '3.0' hibernateValidationVersion = '8.0.2.Final' - jacksonBomVersion = '2.18.2' + jacksonBomVersion = '2.18.3' jaywayJsonPathVersion = '2.9.0' junit4Version = '4.13.2' junitJupiterVersion = '5.11.4' From c87cd0f60d87fbdbd7265f29916d5fc7a74e4066 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Mar 2025 14:22:57 -0500 Subject: [PATCH 703/737] Bump com.uber.nullaway:nullaway in the development-dependencies group Bumps the development-dependencies group with 1 update: [com.uber.nullaway:nullaway](https://github.com/uber/NullAway). Updates `com.uber.nullaway:nullaway` from 0.12.3 to 0.12.4 - [Release notes](https://github.com/uber/NullAway/releases) - [Changelog](https://github.com/uber/NullAway/blob/master/CHANGELOG.md) - [Commits](https://github.com/uber/NullAway/compare/v0.12.3...v0.12.4) --- updated-dependencies: - dependency-name: com.uber.nullaway:nullaway dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c2c2344c83..e4e35bb18c 100644 --- a/build.gradle +++ b/build.gradle @@ -224,7 +224,7 @@ configure(javaProjects) { subproject -> testImplementation 'org.jetbrains.kotlin:kotlin-reflect' testImplementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' - errorprone 'com.uber.nullaway:nullaway:0.12.3' + errorprone 'com.uber.nullaway:nullaway:0.12.4' errorprone 'com.google.errorprone:error_prone_core:2.36.0' } From b90b672ff7de1fbdd9f291dcd51aee2ac7447fcf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Mar 2025 14:24:30 -0500 Subject: [PATCH 704/737] Bump ch.qos.logback:logback-classic from 1.5.16 to 1.5.17 Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.16 to 1.5.17. - [Release notes](https://github.com/qos-ch/logback/releases) - [Commits](https://github.com/qos-ch/logback/compare/v_1.5.16...v_1.5.17) --- updated-dependencies: - dependency-name: ch.qos.logback:logback-classic dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e4e35bb18c..f02e407c7c 100644 --- a/build.gradle +++ b/build.gradle @@ -54,7 +54,7 @@ ext { junitJupiterVersion = '5.11.4' kotlinCoroutinesVersion = '1.10.1' log4jVersion = '2.24.3' - logbackVersion = '1.5.16' + logbackVersion = '1.5.17' micrometerDocsVersion = '1.0.4' micrometerVersion = '1.15.0-SNAPSHOT' micrometerTracingVersion = '1.5.0-SNAPSHOT' From 434410f5030f786868a34ebbe551e3f6486b3f9d Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 4 Mar 2025 15:25:41 -0500 Subject: [PATCH 705/737] GH-2991: Extract `AmqpConnectionFactory` abstraction Fixes: https://github.com/spring-projects/spring-amqp/issues/2991 It turns out to be not very convenient when application fails on initialization due to not having connection with the broker. So, the `AmqpConnectionFactoryBean` is wrong contract and better look into a `ConnectionFactory` abstraction instead. * Rework `AmqpConnectionFactoryBean` with its eager real connection to the `AmqpConnectionFactory` contract and `SingleAmqpConnectionFactory` implementation where real connection only happens when its `getConnection()` is called * Rework all the client components to rely on the new `AmqpConnectionFactory` contract * Make `Publisher` instantiation in a similar to `Connection` manner - on demand, when `RabbitAmqpTemplate` calls its internal `getPublisher()` --- .../client/AmqpConnectionFactory.java | 32 ++++++++ .../amqp/rabbitmq/client/RabbitAmqpAdmin.java | 32 ++++---- .../rabbitmq/client/RabbitAmqpTemplate.java | 72 +++++++++++------ ....java => SingleAmqpConnectionFactory.java} | 78 +++++++++++-------- .../RabbitAmqpListenerContainerFactory.java | 14 ++-- .../listener/RabbitAmqpListenerContainer.java | 14 ++-- .../client/RabbitAmqpTemplateTests.java | 10 +-- .../rabbitmq/client/RabbitAmqpTestBase.java | 15 ++-- .../listener/RabbitAmqpListenerTests.java | 12 +-- 9 files changed, 174 insertions(+), 105 deletions(-) create mode 100644 spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/AmqpConnectionFactory.java rename spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/{AmqpConnectionFactoryBean.java => SingleAmqpConnectionFactory.java} (71%) diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/AmqpConnectionFactory.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/AmqpConnectionFactory.java new file mode 100644 index 0000000000..7ccf89ebd2 --- /dev/null +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/AmqpConnectionFactory.java @@ -0,0 +1,32 @@ +/* + * Copyright 2025 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.rabbitmq.client; + +import com.rabbitmq.client.amqp.Connection; + +/** + * The contract for RabbitMQ AMQP 1.0 {@link Connection} management. + * + * @author Artem Bilan + * + * @since 4.0 + */ +public interface AmqpConnectionFactory { + + Connection getConnection(); + +} diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdmin.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdmin.java index 8394f34ea1..0f93211216 100644 --- a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdmin.java +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdmin.java @@ -25,7 +25,6 @@ import java.util.concurrent.atomic.AtomicReference; import com.rabbitmq.client.amqp.AmqpException; -import com.rabbitmq.client.amqp.Connection; import com.rabbitmq.client.amqp.Management; import org.jspecify.annotations.Nullable; @@ -67,7 +66,7 @@ public class RabbitAmqpAdmin public static final String QUEUE_TYPE = "QUEUE_TYPE"; - private final Connection amqpConnection; + private final AmqpConnectionFactory connectionFactory; private TaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(); @@ -88,8 +87,8 @@ public class RabbitAmqpAdmin private volatile boolean running = false; - public RabbitAmqpAdmin(Connection amqpConnection) { - this.amqpConnection = amqpConnection; + public RabbitAmqpAdmin(AmqpConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; } @Override @@ -231,7 +230,7 @@ private void declareDeclarableBeans() { return; } - try (Management management = this.amqpConnection.management()) { + try (Management management = getManagement()) { exchanges.forEach((exchange) -> doDeclareExchange(management, exchange)); queues.forEach((queue) -> doDeclareQueue(management, queue)); bindings.forEach((binding) -> doDeclareBinding(management, binding)); @@ -273,7 +272,7 @@ private boolean declarableByMe(T dec) { @Override public void declareExchange(Exchange exchange) { - try (Management management = this.amqpConnection.management()) { + try (Management management = getManagement()) { doDeclareExchange(management, exchange); } } @@ -307,7 +306,7 @@ public boolean deleteExchange(String exchangeName) { return false; } - try (Management management = this.amqpConnection.management()) { + try (Management management = getManagement()) { management.exchangeDelete(exchangeName); } return true; @@ -315,7 +314,7 @@ public boolean deleteExchange(String exchangeName) { @Override public @Nullable Queue declareQueue() { - try (Management management = this.amqpConnection.management()) { + try (Management management = getManagement()) { return doDeclareQueue(management); } } @@ -340,7 +339,7 @@ public boolean deleteExchange(String exchangeName) { @Override public @Nullable String declareQueue(Queue queue) { - try (Management management = this.amqpConnection.management()) { + try (Management management = getManagement()) { return doDeclareQueue(management, queue); } } @@ -378,7 +377,7 @@ public boolean deleteQueue(String queueName) { @ManagedOperation(description = "Delete a queue from the broker if unused and empty (when corresponding arguments are true") public void deleteQueue(String queueName, boolean unused, boolean empty) { - try (Management management = this.amqpConnection.management()) { + try (Management management = getManagement()) { Management.QueueInfo queueInfo = management.queueInfo(queueName); if ((!unused || queueInfo.consumerCount() == 0) && (!empty || queueInfo.messageCount() == 0)) { @@ -402,7 +401,7 @@ public void purgeQueue(String queueName, boolean noWait) { @Override @ManagedOperation(description = "Purge a queue and return the number of messages purged") public int purgeQueue(String queueName) { - try (Management management = this.amqpConnection.management()) { + try (Management management = getManagement()) { management.queuePurge(queueName); } return 0; @@ -410,7 +409,7 @@ public int purgeQueue(String queueName) { @Override public void declareBinding(Binding binding) { - try (Management management = this.amqpConnection.management()) { + try (Management management = getManagement()) { doDeclareBinding(management, binding); } } @@ -441,7 +440,7 @@ public void removeBinding(Binding binding) { return; } - try (Management management = this.amqpConnection.management()) { + try (Management management = getManagement()) { Management.UnbindSpecification unbindSpecification = management.unbind() .sourceExchange(binding.getExchange()) @@ -481,7 +480,7 @@ public void removeBinding(Binding binding) { @Override public @Nullable QueueInformation getQueueInfo(String queueName) { - try (Management management = this.amqpConnection.management()) { + try (Management management = getManagement()) { Management.QueueInfo queueInfo = management.queueInfo(queueName); QueueInformation queueInformation = new QueueInformation(queueInfo.name(), queueInfo.messageCount(), queueInfo.consumerCount()); @@ -490,6 +489,10 @@ public void removeBinding(Binding binding) { } } + private Management getManagement() { + return this.connectionFactory.getConnection().management(); + } + private void logOrRethrowDeclarationException(@Nullable Declarable element, String elementType, T t) throws T { @@ -566,5 +569,4 @@ else if (declarable instanceof Binding binding) { }); } - } diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplate.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplate.java index c2a6eba633..8df58e39ed 100644 --- a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplate.java +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplate.java @@ -19,12 +19,12 @@ import java.time.Duration; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; -import com.rabbitmq.client.amqp.Connection; import com.rabbitmq.client.amqp.Consumer; import com.rabbitmq.client.amqp.Environment; import com.rabbitmq.client.amqp.Publisher; -import com.rabbitmq.client.amqp.PublisherBuilder; import com.rabbitmq.client.amqp.Resource; import org.jspecify.annotations.Nullable; @@ -42,7 +42,6 @@ import org.springframework.amqp.support.converter.SmartMessageConverter; import org.springframework.amqp.utils.JavaUtils; import org.springframework.beans.factory.DisposableBean; -import org.springframework.beans.factory.InitializingBean; import org.springframework.core.ParameterizedTypeReference; import org.springframework.util.Assert; @@ -54,14 +53,13 @@ * * @since 4.0 */ -public class RabbitAmqpTemplate implements AsyncAmqpTemplate, InitializingBean, DisposableBean { +public class RabbitAmqpTemplate implements AsyncAmqpTemplate, DisposableBean { - private final Connection connection; + private final AmqpConnectionFactory connectionFactory; - private final PublisherBuilder publisherBuilder; + private final Lock instanceLock = new ReentrantLock(); - @SuppressWarnings("NullAway.Init") - private Publisher publisher; + private @Nullable Object publisher; private MessageConverter messageConverter = new SimpleMessageConverter(); @@ -73,17 +71,20 @@ public class RabbitAmqpTemplate implements AsyncAmqpTemplate, InitializingBean, private @Nullable String defaultReceiveQueue; - public RabbitAmqpTemplate(Connection amqpConnection) { - this.connection = amqpConnection; - this.publisherBuilder = amqpConnection.publisherBuilder(); + private Resource.StateListener @Nullable [] stateListeners; + + private Duration publishTimeout = Duration.ofSeconds(60); + + public RabbitAmqpTemplate(AmqpConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; } public void setListeners(Resource.StateListener... listeners) { - this.publisherBuilder.listeners(listeners); + this.stateListeners = listeners; } public void setPublishTimeout(Duration timeout) { - this.publisherBuilder.publishTimeout(timeout); + this.publishTimeout = timeout; } /** @@ -100,15 +101,15 @@ public void setExchange(String exchange) { /** * Set a default routing key. * Mutually exclusive with {@link #setQueue(String)}. - * @param key the default routing key. + * @param routingKey the default routing key. */ - public void setKey(String key) { - this.defaultRoutingKey = key; + public void setRoutingKey(String routingKey) { + this.defaultRoutingKey = routingKey; } /** * Set default queue for publishing. - * Mutually exclusive with {@link #setExchange(String)} and {@link #setKey(String)}. + * Mutually exclusive with {@link #setExchange(String)} and {@link #setRoutingKey(String)}. * @param queue the default queue. */ public void setQueue(String queue) { @@ -137,14 +138,36 @@ private String getRequiredQueue() throws IllegalStateException { return name; } - @Override - public void afterPropertiesSet() { - this.publisher = this.publisherBuilder.build(); + private Publisher getPublisher() { + Object publisherToReturn = this.publisher; + if (publisherToReturn == null) { + this.instanceLock.lock(); + try { + publisherToReturn = this.publisher; + if (publisherToReturn == null) { + publisherToReturn = + this.connectionFactory.getConnection() + .publisherBuilder() + .listeners(this.stateListeners) + .publishTimeout(this.publishTimeout) + .build(); + this.publisher = publisherToReturn; + } + } + finally { + this.instanceLock.unlock(); + } + } + return (Publisher) publisherToReturn; } @Override public void destroy() { - this.publisher.close(); + Object publisherToClose = this.publisher; + if (publisherToClose != null) { + ((Publisher) publisherToClose).close(); + this.publisher = null; + } } /** @@ -178,7 +201,7 @@ public CompletableFuture send(String exchange, @Nullable String routing private CompletableFuture doSend(@Nullable String exchange, @Nullable String routingKey, @Nullable String queue, Message message) { - com.rabbitmq.client.amqp.Message amqpMessage = this.publisher.message(); + com.rabbitmq.client.amqp.Message amqpMessage = getPublisher().message(); com.rabbitmq.client.amqp.Message.MessageAddressBuilder address = amqpMessage.toAddress(); JavaUtils.INSTANCE .acceptIfNotNull(exchange, address::exchange) @@ -191,7 +214,7 @@ private CompletableFuture doSend(@Nullable String exchange, @Nullable S CompletableFuture publishResult = new CompletableFuture<>(); - this.publisher.publish(amqpMessage, + getPublisher().publish(amqpMessage, (context) -> { switch (context.status()) { case ACCEPTED -> publishResult.complete(true); @@ -271,7 +294,8 @@ public CompletableFuture receive(String queueName) { CompletableFuture messageFuture = new CompletableFuture<>(); Consumer consumer = - this.connection.consumerBuilder() + this.connectionFactory.getConnection() + .consumerBuilder() .queue(queueName) .initialCredits(1) .priority(10) diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/AmqpConnectionFactoryBean.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/SingleAmqpConnectionFactory.java similarity index 71% rename from spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/AmqpConnectionFactoryBean.java rename to spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/SingleAmqpConnectionFactory.java index b0769f5e67..c9d0c188ab 100644 --- a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/AmqpConnectionFactoryBean.java +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/SingleAmqpConnectionFactory.java @@ -17,6 +17,8 @@ package org.springframework.amqp.rabbitmq.client; import java.time.Duration; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; import javax.net.ssl.SSLContext; @@ -32,118 +34,132 @@ import com.rabbitmq.client.amqp.Resource; import org.jspecify.annotations.Nullable; -import org.springframework.beans.factory.config.AbstractFactoryBean; +import org.springframework.beans.factory.DisposableBean; /** - * The {@link AbstractFactoryBean} for RabbitMQ AMQP 1.0 {@link Connection}. - * A Spring-friendly wrapper around {@link Environment#connectionBuilder()}; + * The {@link AmqpConnectionFactory} implementation to hold a single, shared {@link Connection} instance. * * @author Artem Bilan * * @since 4.0 */ -public class AmqpConnectionFactoryBean extends AbstractFactoryBean { +public class SingleAmqpConnectionFactory implements AmqpConnectionFactory, DisposableBean { private final ConnectionBuilder connectionBuilder; - public AmqpConnectionFactoryBean(Environment amqpEnvironment) { + private final Lock instanceLock = new ReentrantLock(); + + private volatile @Nullable Connection connection; + + public SingleAmqpConnectionFactory(Environment amqpEnvironment) { this.connectionBuilder = amqpEnvironment.connectionBuilder(); } - public AmqpConnectionFactoryBean setHost(String host) { + public SingleAmqpConnectionFactory setHost(String host) { this.connectionBuilder.host(host); return this; } - public AmqpConnectionFactoryBean setPort(int port) { + public SingleAmqpConnectionFactory setPort(int port) { this.connectionBuilder.port(port); return this; } - public AmqpConnectionFactoryBean setUsername(String username) { + public SingleAmqpConnectionFactory setUsername(String username) { this.connectionBuilder.username(username); return this; } - public AmqpConnectionFactoryBean setPassword(String password) { + public SingleAmqpConnectionFactory setPassword(String password) { this.connectionBuilder.password(password); return this; } - public AmqpConnectionFactoryBean setVirtualHost(String virtualHost) { + public SingleAmqpConnectionFactory setVirtualHost(String virtualHost) { this.connectionBuilder.virtualHost(virtualHost); return this; } - public AmqpConnectionFactoryBean setUri(String uri) { + public SingleAmqpConnectionFactory setUri(String uri) { this.connectionBuilder.uri(uri); return this; } - public AmqpConnectionFactoryBean setUris(String... uris) { + public SingleAmqpConnectionFactory setUris(String... uris) { this.connectionBuilder.uris(uris); return this; } - public AmqpConnectionFactoryBean setIdleTimeout(Duration idleTimeout) { + public SingleAmqpConnectionFactory setIdleTimeout(Duration idleTimeout) { this.connectionBuilder.idleTimeout(idleTimeout); return this; } - public AmqpConnectionFactoryBean setAddressSelector(AddressSelector addressSelector) { + public SingleAmqpConnectionFactory setAddressSelector(AddressSelector addressSelector) { this.connectionBuilder.addressSelector(addressSelector); return this; } - public AmqpConnectionFactoryBean setCredentialsProvider(CredentialsProvider credentialsProvider) { + public SingleAmqpConnectionFactory setCredentialsProvider(CredentialsProvider credentialsProvider) { this.connectionBuilder.credentialsProvider(credentialsProvider); return this; } - public AmqpConnectionFactoryBean setSaslMechanism(SaslMechanism saslMechanism) { + public SingleAmqpConnectionFactory setSaslMechanism(SaslMechanism saslMechanism) { this.connectionBuilder.saslMechanism(saslMechanism.name()); return this; } - public AmqpConnectionFactoryBean setTls(Consumer tlsCustomizer) { + public SingleAmqpConnectionFactory setTls(Consumer tlsCustomizer) { tlsCustomizer.accept(new Tls(this.connectionBuilder.tls())); return this; } - public AmqpConnectionFactoryBean setAffinity(Consumer affinityCustomizer) { + public SingleAmqpConnectionFactory setAffinity(Consumer affinityCustomizer) { affinityCustomizer.accept(new Affinity(this.connectionBuilder.affinity())); return this; } - public AmqpConnectionFactoryBean setOAuth2(Consumer oauth2Customizer) { + public SingleAmqpConnectionFactory setOAuth2(Consumer oauth2Customizer) { oauth2Customizer.accept(new OAuth2(this.connectionBuilder.oauth2())); return this; } - public AmqpConnectionFactoryBean setRecovery(Consumer recoveryCustomizer) { + public SingleAmqpConnectionFactory setRecovery(Consumer recoveryCustomizer) { recoveryCustomizer.accept(new Recovery(this.connectionBuilder.recovery())); return this; } - public AmqpConnectionFactoryBean setListeners(Resource.StateListener... listeners) { + public SingleAmqpConnectionFactory setListeners(Resource.StateListener... listeners) { this.connectionBuilder.listeners(listeners); return this; } @Override - public @Nullable Class getObjectType() { - return Connection.class; - } - - @Override - protected Connection createInstance() { - return this.connectionBuilder.build(); + public Connection getConnection() { + Connection connectionToReturn = this.connection; + if (connectionToReturn == null) { + this.instanceLock.lock(); + try { + connectionToReturn = this.connection; + if (connectionToReturn == null) { + connectionToReturn = this.connectionBuilder.build(); + this.connection = connectionToReturn; + } + } + finally { + this.instanceLock.unlock(); + } + } + return connectionToReturn; } @Override - protected void destroyInstance(@Nullable Connection instance) { - if (instance != null) { - instance.close(); + public void destroy() { + Connection connectionToClose = this.connection; + if (connectionToClose != null) { + connectionToClose.close(); + this.connection = null; } } diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/config/RabbitAmqpListenerContainerFactory.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/config/RabbitAmqpListenerContainerFactory.java index 6195a2167d..17235a0449 100644 --- a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/config/RabbitAmqpListenerContainerFactory.java +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/config/RabbitAmqpListenerContainerFactory.java @@ -18,7 +18,6 @@ import java.util.Arrays; -import com.rabbitmq.client.amqp.Connection; import org.jspecify.annotations.Nullable; import org.springframework.amqp.core.MessageListener; @@ -27,6 +26,7 @@ import org.springframework.amqp.rabbit.config.ContainerCustomizer; import org.springframework.amqp.rabbit.listener.MethodRabbitListenerEndpoint; import org.springframework.amqp.rabbit.listener.RabbitListenerEndpoint; +import org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory; import org.springframework.amqp.rabbitmq.client.listener.RabbitAmqpListenerContainer; import org.springframework.amqp.rabbitmq.client.listener.RabbitAmqpMessageListenerAdapter; import org.springframework.amqp.utils.JavaUtils; @@ -45,7 +45,7 @@ public class RabbitAmqpListenerContainerFactory extends BaseRabbitListenerContainerFactory { - private final Connection connection; + private final AmqpConnectionFactory connectionFactory; private @Nullable ContainerCustomizer containerCustomizer; @@ -58,11 +58,11 @@ public class RabbitAmqpListenerContainerFactory private @Nullable TaskScheduler taskScheduler; /** - * Construct an instance using the provided amqpConnection. - * @param amqpConnection the connection. + * Construct an instance using the provided {@link AmqpConnectionFactory}. + * @param connectionFactory the connection. */ - public RabbitAmqpListenerContainerFactory(Connection amqpConnection) { - this.connection = amqpConnection; + public RabbitAmqpListenerContainerFactory(AmqpConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; } /** @@ -154,7 +154,7 @@ public RabbitAmqpListenerContainer createListenerContainer(@Nullable RabbitListe } protected RabbitAmqpListenerContainer createContainerInstance() { - return new RabbitAmqpListenerContainer(this.connection); + return new RabbitAmqpListenerContainer(this.connectionFactory); } } diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerContainer.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerContainer.java index eac1699933..9ab6d8a67a 100644 --- a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerContainer.java +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerContainer.java @@ -45,6 +45,7 @@ import org.springframework.amqp.rabbit.listener.ConditionalRejectingErrorHandler; import org.springframework.amqp.rabbit.listener.MessageListenerContainer; import org.springframework.amqp.rabbit.listener.support.ContainerUtils; +import org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory; import org.springframework.amqp.rabbitmq.client.RabbitAmqpUtils; import org.springframework.amqp.support.postprocessor.MessagePostProcessorUtils; import org.springframework.aop.framework.ProxyFactory; @@ -74,7 +75,7 @@ public class RabbitAmqpListenerContainer implements MessageListenerContainer, Be private final Lock lock = new ReentrantLock(); - private final Connection connection; + private final AmqpConnectionFactory connectionFactory; private final MultiValueMap queueToConsumers = new LinkedMultiValueMap<>(); @@ -119,11 +120,11 @@ public class RabbitAmqpListenerContainer implements MessageListenerContainer, Be private boolean internalTaskScheduler = true; /** - * Construct an instance using the provided connection. - * @param connection to use. + * Construct an instance based on the provided {@link AmqpConnectionFactory}. + * @param connectionFactory to use. */ - public RabbitAmqpListenerContainer(Connection connection) { - this.connection = connection; + public RabbitAmqpListenerContainer(AmqpConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; } @Override @@ -307,10 +308,11 @@ public void start() { this.lock.lock(); try { if (this.queueToConsumers.isEmpty()) { + Connection connection = this.connectionFactory.getConnection(); for (String queue : this.queues) { for (int i = 0; i < this.consumersPerQueue; i++) { Consumer consumer = - this.connection.consumerBuilder() + connection.consumerBuilder() .queue(queue) .priority(this.priority) .initialCredits(this.initialCredits) diff --git a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplateTests.java b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplateTests.java index c4dc11460d..afd021cdb4 100644 --- a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplateTests.java +++ b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplateTests.java @@ -18,7 +18,6 @@ import java.time.Duration; -import com.rabbitmq.client.amqp.Connection; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -28,7 +27,6 @@ import org.springframework.amqp.core.DirectExchange; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.Queue; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.test.context.ContextConfiguration; @@ -44,15 +42,11 @@ @ContextConfiguration public class RabbitAmqpTemplateTests extends RabbitAmqpTestBase { - @Autowired - Connection connection; - RabbitAmqpTemplate rabbitAmqpTemplate; @BeforeEach void setUp() { - this.rabbitAmqpTemplate = new RabbitAmqpTemplate(this.connection); - this.rabbitAmqpTemplate.afterPropertiesSet(); + this.rabbitAmqpTemplate = new RabbitAmqpTemplate(this.connectionFactory); } @AfterEach @@ -74,7 +68,7 @@ void illegalStateOnNoDefaults() { @Test void defaultExchangeAndRoutingKey() { this.rabbitAmqpTemplate.setExchange("e1"); - this.rabbitAmqpTemplate.setKey("k1"); + this.rabbitAmqpTemplate.setRoutingKey("k1"); assertThat(this.rabbitAmqpTemplate.convertAndSend("test1")) .succeedsWithin(Duration.ofSeconds(10)); diff --git a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTestBase.java b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTestBase.java index cdd8cc0392..5d580e8403 100644 --- a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTestBase.java +++ b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTestBase.java @@ -20,7 +20,6 @@ import java.util.List; import java.util.stream.Stream; -import com.rabbitmq.client.amqp.Connection; import com.rabbitmq.client.amqp.Environment; import com.rabbitmq.client.amqp.impl.AmqpEnvironmentBuilder; @@ -51,7 +50,7 @@ public abstract class RabbitAmqpTestBase extends AbstractTestContainerTests { protected Environment environment; @Autowired - protected Connection connection; + protected AmqpConnectionFactory connectionFactory; @Autowired protected RabbitAmqpAdmin admin; @@ -81,18 +80,18 @@ Environment environment() { } @Bean - AmqpConnectionFactoryBean connection(Environment environment) { - return new AmqpConnectionFactoryBean(environment); + AmqpConnectionFactory connectionFactory(Environment environment) { + return new SingleAmqpConnectionFactory(environment); } @Bean - RabbitAmqpAdmin admin(Connection connection) { - return new RabbitAmqpAdmin(connection); + RabbitAmqpAdmin admin(AmqpConnectionFactory connectionFactory) { + return new RabbitAmqpAdmin(connectionFactory); } @Bean - RabbitAmqpTemplate rabbitTemplate(Connection connection) { - return new RabbitAmqpTemplate(connection); + RabbitAmqpTemplate rabbitTemplate(AmqpConnectionFactory connectionFactory) { + return new RabbitAmqpTemplate(connectionFactory); } volatile boolean running; diff --git a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java index 22af681df2..9a37503496 100644 --- a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java +++ b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java @@ -25,7 +25,6 @@ import java.util.concurrent.TimeUnit; import java.util.stream.IntStream; -import com.rabbitmq.client.amqp.Connection; import com.rabbitmq.client.amqp.Consumer; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; @@ -41,6 +40,7 @@ import org.springframework.amqp.rabbit.annotation.RabbitListenerAnnotationBeanPostProcessor; import org.springframework.amqp.rabbit.listener.MessageListenerContainer; import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry; +import org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory; import org.springframework.amqp.rabbitmq.client.RabbitAmqpTestBase; import org.springframework.amqp.rabbitmq.client.config.RabbitAmqpListenerContainerFactory; import org.springframework.amqp.support.converter.MessageConversionException; @@ -180,8 +180,8 @@ Queue q3() { } @Bean(RabbitListenerAnnotationBeanPostProcessor.DEFAULT_RABBIT_LISTENER_CONTAINER_FACTORY_BEAN_NAME) - RabbitAmqpListenerContainerFactory rabbitAmqpListenerContainerFactory(Connection connection) { - return new RabbitAmqpListenerContainerFactory(connection); + RabbitAmqpListenerContainerFactory rabbitAmqpListenerContainerFactory(AmqpConnectionFactory connectionFactory) { + return new RabbitAmqpListenerContainerFactory(connectionFactory); } List received = Collections.synchronizedList(new ArrayList<>()); @@ -224,11 +224,11 @@ ThreadPoolTaskScheduler taskScheduler() { } @Bean - RabbitAmqpListenerContainerFactory batchRabbitAmqpListenerContainerFactory(Connection connection, - ThreadPoolTaskScheduler taskScheduler) { + RabbitAmqpListenerContainerFactory batchRabbitAmqpListenerContainerFactory( + AmqpConnectionFactory connectionFactory, ThreadPoolTaskScheduler taskScheduler) { RabbitAmqpListenerContainerFactory rabbitAmqpListenerContainerFactory = - new RabbitAmqpListenerContainerFactory(connection); + new RabbitAmqpListenerContainerFactory(connectionFactory); rabbitAmqpListenerContainerFactory.setTaskScheduler(taskScheduler); rabbitAmqpListenerContainerFactory.setBatchSize(10); rabbitAmqpListenerContainerFactory.setBatchReceiveTimeout(1000L); From 0e011878f5a22360b5aedf71c7e7b737d68d2841 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 6 Mar 2025 15:51:04 -0500 Subject: [PATCH 706/737] GH-3002: Add RPC support to `RabbitAmqpTemplate` Fixes: https://github.com/spring-projects/spring-amqp/issues/3002 * Implement `sendAndReceive` & `receiveAndReply` operations in the `RabbitAmqpTemplate` * Expose contracts to the `AsyncAmqpTemplate` * Some NullAway fixes for `AsyncAmqpTemplate` hierarchy * Move DLQ objects for testing to the common `RabbitAmqpTestBase` --- .../amqp/core/AsyncAmqpTemplate.java | 28 +- .../amqp/rabbit/AsyncRabbitTemplate.java | 2 +- .../rabbitmq/client/RabbitAmqpTemplate.java | 368 ++++++++++++++---- .../amqp/rabbitmq/client/RabbitAmqpUtils.java | 3 +- .../client/RabbitAmqpTemplateTests.java | 80 +++- .../rabbitmq/client/RabbitAmqpTestBase.java | 18 + .../listener/RabbitAmqpListenerTests.java | 18 - .../src/test/resources/log4j2-test.xml | 1 + 8 files changed, 412 insertions(+), 106 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate.java index 5f7556fb70..90dfe728c4 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate.java @@ -92,11 +92,30 @@ default CompletableFuture receiveAndConvert(String queueName) { throw new UnsupportedOperationException(); } - default CompletableFuture receiveAndConvert(ParameterizedTypeReference type) { + default CompletableFuture receiveAndConvert(@Nullable ParameterizedTypeReference type) { throw new UnsupportedOperationException(); } - default CompletableFuture receiveAndConvert(String queueName, ParameterizedTypeReference type) { + default CompletableFuture receiveAndConvert(String queueName, @Nullable ParameterizedTypeReference type) { + throw new UnsupportedOperationException(); + } + + default CompletableFuture receiveAndReply(ReceiveAndReplyCallback callback) { + throw new UnsupportedOperationException(); + } + + /** + * Perform a server-side RPC functionality. + * The request message must have a {@code replyTo} property. + * The request {@code messageId} property is used for correlation. + * The callback might not produce a reply with the meaning nothing to answer. + * @param queueName the queue to consume request. + * @param callback an application callback to handle request and produce reply. + * @return the completion status: true if no errors and reply has been produced. + * @param the request body type. + * @param the response body type + */ + default CompletableFuture receiveAndReply(String queueName, ReceiveAndReplyCallback callback) { throw new UnsupportedOperationException(); } @@ -240,8 +259,9 @@ CompletableFuture convertSendAndReceiveAsType(String exchange, String rou * @param the expected result type. * @return the {@link CompletableFuture}. */ - CompletableFuture convertSendAndReceiveAsType(Object object, MessagePostProcessor messagePostProcessor, - ParameterizedTypeReference responseType); + CompletableFuture convertSendAndReceiveAsType(Object object, + @Nullable MessagePostProcessor messagePostProcessor, + @Nullable ParameterizedTypeReference responseType); /** * Convert the object to a message and send it to the default exchange with the diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java index 0972747d42..cb3e8a4ceb 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/AsyncRabbitTemplate.java @@ -464,7 +464,7 @@ public RabbitConverterFuture convertSendAndReceiveAsType(String exchange, @Override public RabbitConverterFuture convertSendAndReceiveAsType(Object object, - MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) { + @Nullable MessagePostProcessor messagePostProcessor, @Nullable ParameterizedTypeReference responseType) { return convertSendAndReceiveAsType(this.template.getExchange(), this.template.getRoutingKey(), object, messagePostProcessor, responseType); diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplate.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplate.java index 8df58e39ed..559f744b43 100644 --- a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplate.java +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplate.java @@ -17,25 +17,28 @@ package org.springframework.amqp.rabbitmq.client; import java.time.Duration; +import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; import com.rabbitmq.client.amqp.Consumer; import com.rabbitmq.client.amqp.Environment; import com.rabbitmq.client.amqp.Publisher; import com.rabbitmq.client.amqp.Resource; +import com.rabbitmq.client.amqp.RpcClient; import org.jspecify.annotations.Nullable; -import org.springframework.amqp.AmqpException; +import org.springframework.amqp.AmqpIllegalStateException; import org.springframework.amqp.core.AmqpTemplate; import org.springframework.amqp.core.AsyncAmqpTemplate; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessagePostProcessor; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.core.ReceiveAndReplyCallback; -import org.springframework.amqp.core.ReplyToAddressCallback; +import org.springframework.amqp.core.ReceiveAndReplyMessageCallback; import org.springframework.amqp.rabbit.core.AmqpNackReceivedException; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.amqp.support.converter.SimpleMessageConverter; @@ -43,7 +46,9 @@ import org.springframework.amqp.utils.JavaUtils; import org.springframework.beans.factory.DisposableBean; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.log.LogAccessor; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * The {@link AmqpTemplate} for RabbitMQ AMQP 1.0 protocol support. @@ -55,6 +60,8 @@ */ public class RabbitAmqpTemplate implements AsyncAmqpTemplate, DisposableBean { + private static final LogAccessor LOG = new LogAccessor(RabbitAmqpAdmin.class); + private final AmqpConnectionFactory connectionFactory; private final Lock instanceLock = new ReentrantLock(); @@ -71,10 +78,14 @@ public class RabbitAmqpTemplate implements AsyncAmqpTemplate, DisposableBean { private @Nullable String defaultReceiveQueue; + private @Nullable String defaultReplyToQueue; + private Resource.StateListener @Nullable [] stateListeners; private Duration publishTimeout = Duration.ofSeconds(60); + private Duration completionTimeout = Duration.ofSeconds(60); + public RabbitAmqpTemplate(AmqpConnectionFactory connectionFactory) { this.connectionFactory = connectionFactory; } @@ -87,6 +98,19 @@ public void setPublishTimeout(Duration timeout) { this.publishTimeout = timeout; } + /** + * Set a duration for {@link CompletableFuture#orTimeout(long, TimeUnit)} on returns. + * There is no {@link CompletableFuture} API like {@code onTimeout()} requested + * from the {@link CompletableFuture#get(long, TimeUnit)}, + * but used in operations AMQP resources have to be closed eventually independently + * of the {@link CompletableFuture} fulfilment. + * Defaults to 1 minute. + * @param completionTimeout duration for future completions. + */ + public void setCompletionTimeout(Duration completionTimeout) { + this.completionTimeout = completionTimeout; + } + /** * Set a default exchange for publishing. * Cannot be real default AMQP exchange. @@ -117,19 +141,27 @@ public void setQueue(String queue) { } /** - * Set a converter for {@link #convertAndSend(Object)} operations. - * @param messageConverter the converter. + * The name of the default queue to receive messages from when none is specified explicitly. + * @param queue the default queue name to use for receive operation. */ - public void setMessageConverter(MessageConverter messageConverter) { - this.messageConverter = messageConverter; + public void setReceiveQueue(String queue) { + this.defaultReceiveQueue = queue; } /** - * The name of the default queue to receive messages from when none is specified explicitly. - * @param queue the default queue name to use for receive operation. + * The name of the default queue to receive replies from when none is specified explicitly. + * @param queue the default queue name to use for send-n-receive operation. */ - public void setDefaultReceiveQueue(String queue) { - this.defaultReceiveQueue = queue; + public void setReplyToQueue(String queue) { + this.defaultReplyToQueue = queue; + } + + /** + * Set a converter for {@link #convertAndSend(Object)} operations. + * @param messageConverter the converter. + */ + public void setMessageConverter(MessageConverter messageConverter) { + this.messageConverter = messageConverter; } private String getRequiredQueue() throws IllegalStateException { @@ -178,7 +210,7 @@ public void destroy() { @Override public CompletableFuture send(Message message) { Assert.state(this.defaultExchange != null || this.defaultQueue != null, - "For send with defaults, an 'exchange' (and optional 'key') or 'queue' must be provided"); + "For send with defaults, an 'exchange' (and optional 'routingKey') or 'queue' must be provided"); return doSend(this.defaultExchange, this.defaultRoutingKey, this.defaultQueue, message); } @@ -201,16 +233,8 @@ public CompletableFuture send(String exchange, @Nullable String routing private CompletableFuture doSend(@Nullable String exchange, @Nullable String routingKey, @Nullable String queue, Message message) { - com.rabbitmq.client.amqp.Message amqpMessage = getPublisher().message(); - com.rabbitmq.client.amqp.Message.MessageAddressBuilder address = amqpMessage.toAddress(); - JavaUtils.INSTANCE - .acceptIfNotNull(exchange, address::exchange) - .acceptIfNotNull(routingKey, address::key) - .acceptIfNotNull(queue, address::queue); - - amqpMessage = address.message(); - - RabbitAmqpUtils.toAmqpMessage(message, amqpMessage); + com.rabbitmq.client.amqp.Message amqpMessage = + toAmqpMessage(exchange, routingKey, queue, message, getPublisher()::message); CompletableFuture publishResult = new CompletableFuture<>(); @@ -235,7 +259,7 @@ private CompletableFuture doSend(@Nullable String exchange, @Nullable S @Override public CompletableFuture convertAndSend(Object message) { Assert.state(this.defaultExchange != null || this.defaultQueue != null, - "For send with defaults, an 'exchange' (and optional 'key') or 'queue' must be provided"); + "For send with defaults, an 'exchange' (and optional 'routingKey') or 'queue' must be provided"); return doConvertAndSend(this.defaultExchange, this.defaultRoutingKey, this.defaultQueue, message, null); } @@ -273,10 +297,7 @@ public CompletableFuture convertAndSend(String exchange, @Nullable Stri private CompletableFuture doConvertAndSend(@Nullable String exchange, @Nullable String routingKey, @Nullable String queue, Object data, @Nullable MessagePostProcessor messagePostProcessor) { - Message message = - data instanceof Message - ? (Message) data - : this.messageConverter.toMessage(data, new MessageProperties()); + Message message = convertToMessageIfNecessary(data); if (messagePostProcessor != null) { message = messagePostProcessor.postProcessMessage(message); } @@ -288,6 +309,13 @@ public CompletableFuture receive() { return receive(getRequiredQueue()); } + /** + * Request a head message from the provided queue. + * A returned {@link CompletableFuture} timeouts after {@link #setCompletionTimeout(Duration)}. + * @param queueName the queue to consume message from. + * @return the future with a received message. + * @see #setCompletionTimeout(Duration) + */ @SuppressWarnings("try") @Override public CompletableFuture receive(String queueName) { @@ -306,47 +334,54 @@ public CompletableFuture receive(String queueName) { .build(); return messageFuture - .orTimeout(1, TimeUnit.MINUTES) + .orTimeout(this.completionTimeout.toMillis(), TimeUnit.MILLISECONDS) .whenComplete((message, exception) -> consumer.close()); } @Override public CompletableFuture receiveAndConvert() { - return receiveAndConvert(getRequiredQueue()); + return receiveAndConvert((ParameterizedTypeReference) null); } @Override public CompletableFuture receiveAndConvert(String queueName) { - return receive(queueName) - .thenApply(this.messageConverter::fromMessage); + return receiveAndConvert(queueName, null); } /** - * Receive a message from {@link #setDefaultReceiveQueue(String)} and convert its body + * Receive a message from {@link #setReceiveQueue(String)} and convert its body * to the expected type. * The {@link #setMessageConverter(MessageConverter)} must be an implementation of {@link SmartMessageConverter}. * @param type the type to covert received result. * @return the CompletableFuture with a result. */ @Override - public CompletableFuture receiveAndConvert(ParameterizedTypeReference type) { + public CompletableFuture receiveAndConvert(@Nullable ParameterizedTypeReference type) { return receiveAndConvert(getRequiredQueue(), type); } /** - * Receive a message from {@link #setDefaultReceiveQueue(String)} and convert its body + * Receive a message from {@link #setReceiveQueue(String)} and convert its body * to the expected type. * The {@link #setMessageConverter(MessageConverter)} must be an implementation of {@link SmartMessageConverter}. * @param queueName the queue to consume message from. * @param type the type to covert received result. * @return the CompletableFuture with a result. */ - @SuppressWarnings("unchecked") @Override - public CompletableFuture receiveAndConvert(String queueName, ParameterizedTypeReference type) { - SmartMessageConverter smartMessageConverter = getRequiredSmartMessageConverter(); + public CompletableFuture receiveAndConvert(String queueName, @Nullable ParameterizedTypeReference type) { return receive(queueName) - .thenApply((message) -> (T) smartMessageConverter.fromMessage(message, type)); + .thenApply((message) -> convertReply(message, type)); + } + + @SuppressWarnings("unchecked") + private T convertReply(Message message, @Nullable ParameterizedTypeReference type) { + if (type != null) { + return (T) getRequiredSmartMessageConverter().fromMessage(message, type); + } + else { + return (T) this.messageConverter.fromMessage(message); + } } private SmartMessageConverter getRequiredSmartMessageConverter() throws IllegalStateException { @@ -355,103 +390,280 @@ private SmartMessageConverter getRequiredSmartMessageConverter() throws IllegalS return (SmartMessageConverter) this.messageConverter; } - public boolean receiveAndReply(ReceiveAndReplyCallback callback) throws AmqpException { - throw new UnsupportedOperationException(); + @Override + public CompletableFuture receiveAndReply(ReceiveAndReplyCallback callback) { + return receiveAndReply(getRequiredQueue(), callback); } - public boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback) throws AmqpException { - throw new UnsupportedOperationException(); - } + @Override + @SuppressWarnings("try") + public CompletableFuture receiveAndReply(String queueName, ReceiveAndReplyCallback callback) { + CompletableFuture rpcFuture = new CompletableFuture<>(); + + Consumer.MessageHandler consumerHandler = + (context, message) -> { + Message requestMessage = RabbitAmqpUtils.fromAmqpMessage(message, null); + try { + Object messageId = message.messageId(); + Assert.notNull(messageId, + "The 'message-id' property has to be set on request. Used for reply correlation."); + String replyTo = message.replyTo(); + Assert.hasText(replyTo, + "The 'reply-to' property has to be set on request. Used for reply publishing."); + Message reply = handleRequestAndProduceReply(requestMessage, callback); + if (reply == null) { + LOG.info(() -> "No reply for request: " + requestMessage); + context.accept(); + rpcFuture.complete(false); + } + else { + com.rabbitmq.client.amqp.Message replyMessage = getPublisher().message(); + RabbitAmqpUtils.toAmqpMessage(reply, replyMessage); + replyMessage.correlationId(messageId); + replyMessage.to(replyTo); + getPublisher().publish(replyMessage, (ctx) -> { + }); + context.accept(); + rpcFuture.complete(true); + } + } + catch (Exception ex) { + context.discard(); + rpcFuture.completeExceptionally( + new AmqpIllegalStateException("Failed to process RPC request: " + requestMessage, ex)); + } + }; - public boolean receiveAndReply(ReceiveAndReplyCallback callback, String replyExchange, String replyRoutingKey) throws AmqpException { - throw new UnsupportedOperationException(); - } + Consumer consumer = + this.connectionFactory.getConnection() + .consumerBuilder() + .queue(queueName) + .initialCredits(1) + .priority(10) + .messageHandler(consumerHandler) + .build(); - public boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback, String replyExchange, String replyRoutingKey) throws AmqpException { - throw new UnsupportedOperationException(); + return rpcFuture + .orTimeout(this.completionTimeout.toMillis(), TimeUnit.MILLISECONDS) + .whenComplete((message, exception) -> consumer.close()); } - public boolean receiveAndReply(ReceiveAndReplyCallback callback, ReplyToAddressCallback replyToAddressCallback) throws AmqpException { - throw new UnsupportedOperationException(); + @SuppressWarnings("unchecked") + private @Nullable Message handleRequestAndProduceReply(Message requestMessage, + ReceiveAndReplyCallback callback) { + + Object receive = requestMessage; + if (!(ReceiveAndReplyMessageCallback.class.isAssignableFrom(callback.getClass()))) { + receive = this.messageConverter.fromMessage(requestMessage); + } + + S reply; + try { + reply = callback.handle((R) receive); + } + catch (ClassCastException ex) { + StackTraceElement[] trace = ex.getStackTrace(); + if (trace[0].getMethodName().equals("handle") + && Objects.equals(trace[1].getFileName(), "RabbitAmqpTemplate.java")) { + + throw new IllegalArgumentException("ReceiveAndReplyCallback '" + callback + + "' can't handle received object '" + receive + "'", ex); + } + else { + throw ex; + } + } + + if (reply != null) { + return convertToMessageIfNecessary(reply); + } + return null; } - public boolean receiveAndReply(String queueName, ReceiveAndReplyCallback callback, ReplyToAddressCallback replyToAddressCallback) throws AmqpException { - throw new UnsupportedOperationException(); + private Message convertToMessageIfNecessary(Object data) { + if (data instanceof Message msg) { + return msg; + } + else { + return this.messageConverter.toMessage(data, new MessageProperties()); + } } @Override public CompletableFuture sendAndReceive(Message message) { - throw new UnsupportedOperationException(); + Assert.state(this.defaultExchange != null || this.defaultQueue != null, + "For send-n-receive with defaults, an 'exchange' (and optional 'routingKey') or 'queue' must be provided"); + return doSendAndReceive(this.defaultExchange, this.defaultRoutingKey, this.defaultQueue, message); } @Override - public CompletableFuture sendAndReceive(String routingKey, Message message) { - throw new UnsupportedOperationException(); + public CompletableFuture sendAndReceive(String exchange, @Nullable String routingKey, Message message) { + return doSendAndReceive(exchange, routingKey != null ? routingKey : this.defaultRoutingKey, null, message); } @Override - public CompletableFuture sendAndReceive(String exchange, String routingKey, Message message) { - throw new UnsupportedOperationException(); + public CompletableFuture sendAndReceive(String queue, Message message) { + return doSendAndReceive(null, null, queue, message); + } + + @SuppressWarnings("try") + private CompletableFuture doSendAndReceive(@Nullable String exchange, @Nullable String routingKey, + @Nullable String queue, Message message) { + + MessageProperties messageProperties = message.getMessageProperties(); + String messageId = messageProperties.getMessageId(); + String correlationId = messageProperties.getCorrelationId(); + String replyTo = messageProperties.getReplyTo(); + + // HTTP over AMQP 1.0 extension specification, 5.1: + // To associate a response with a request, the correlation-id value of the response properties + // MUST be set to the message-id value of the request properties. + // So, this supplier will override request message-id, respectively. + // Otherwise, the RpcClient generates correlation-id internally. + Supplier correlationIdSupplier = null; + if (StringUtils.hasText(correlationId)) { + correlationIdSupplier = () -> correlationId; + } + else if (StringUtils.hasText(messageId)) { + correlationIdSupplier = () -> messageId; + } + + // The default reply-to queue, or the one supplied in the message. + // Otherwise, the RpcClient generates one as exclusive and auto-delete. + String replyToQueue = this.defaultReplyToQueue; + if (StringUtils.hasText(replyTo)) { + replyToQueue = replyTo; + } + + RpcClient rpcClient = + this.connectionFactory.getConnection() + .rpcClientBuilder() + .requestTimeout(this.publishTimeout) + .correlationIdSupplier(correlationIdSupplier) + .replyToQueue(replyToQueue) + .build(); + + com.rabbitmq.client.amqp.Message amqpMessage = + toAmqpMessage(exchange, routingKey, queue, message, rpcClient::message); + + return rpcClient.publish(amqpMessage) + .thenApply((reply) -> RabbitAmqpUtils.fromAmqpMessage(reply, null)) + .orTimeout(this.completionTimeout.toMillis(), TimeUnit.MILLISECONDS) + .whenComplete((replyMessage, exception) -> rpcClient.close()); } @Override public CompletableFuture convertSendAndReceive(Object object) { - throw new UnsupportedOperationException(); + return convertSendAndReceiveAsType(object, null, null); } @Override - public CompletableFuture convertSendAndReceive(String routingKey, Object object) { - throw new UnsupportedOperationException(); + public CompletableFuture convertSendAndReceive(String queue, Object object) { + return convertSendAndReceiveAsType(queue, object, null, null); } @Override - public CompletableFuture convertSendAndReceive(String exchange, String routingKey, Object object) { - throw new UnsupportedOperationException(); + public CompletableFuture convertSendAndReceive(String exchange, @Nullable String routingKey, Object object) { + return convertSendAndReceiveAsType(exchange, routingKey, object, null, null); } @Override public CompletableFuture convertSendAndReceive(Object object, MessagePostProcessor messagePostProcessor) { - throw new UnsupportedOperationException(); + return convertSendAndReceiveAsType(object, messagePostProcessor, null); } @Override - public CompletableFuture convertSendAndReceive(String routingKey, Object object, MessagePostProcessor messagePostProcessor) { - throw new UnsupportedOperationException(); + public CompletableFuture convertSendAndReceive(String queue, Object object, + MessagePostProcessor messagePostProcessor) { + + return convertSendAndReceiveAsType(queue, object, messagePostProcessor, null); } @Override - public CompletableFuture convertSendAndReceive(String exchange, String routingKey, Object object, @Nullable MessagePostProcessor messagePostProcessor) { - throw new UnsupportedOperationException(); + public CompletableFuture convertSendAndReceive(String exchange, @Nullable String routingKey, + Object object, @Nullable MessagePostProcessor messagePostProcessor) { + + return convertSendAndReceiveAsType(exchange, routingKey, object, messagePostProcessor, null); } @Override - public CompletableFuture convertSendAndReceiveAsType(Object object, ParameterizedTypeReference responseType) { - throw new UnsupportedOperationException(); + public CompletableFuture convertSendAndReceiveAsType(Object object, + ParameterizedTypeReference responseType) { + + return convertSendAndReceiveAsType(object, null, responseType); } @Override - public CompletableFuture convertSendAndReceiveAsType(String routingKey, Object object, ParameterizedTypeReference responseType) { - throw new UnsupportedOperationException(); + public CompletableFuture convertSendAndReceiveAsType(String queue, Object object, + ParameterizedTypeReference responseType) { + + return convertSendAndReceiveAsType(queue, object, null, responseType); } @Override - public CompletableFuture convertSendAndReceiveAsType(String exchange, String routingKey, Object object, ParameterizedTypeReference responseType) { - throw new UnsupportedOperationException(); + public CompletableFuture convertSendAndReceiveAsType(String exchange, @Nullable String routingKey, + Object object, ParameterizedTypeReference responseType) { + + return convertSendAndReceiveAsType(exchange, routingKey, object, null, responseType); } @Override - public CompletableFuture convertSendAndReceiveAsType(Object object, MessagePostProcessor messagePostProcessor, ParameterizedTypeReference responseType) { - throw new UnsupportedOperationException(); + public CompletableFuture convertSendAndReceiveAsType(Object object, + @Nullable MessagePostProcessor messagePostProcessor, @Nullable ParameterizedTypeReference responseType) { + + Assert.state(this.defaultExchange != null || this.defaultQueue != null, + "For send with defaults, an 'exchange' (and optional 'routingKey') or 'queue' must be provided"); + + return doConvertSendAndReceive(this.defaultExchange, this.defaultRoutingKey, this.defaultQueue, object, + messagePostProcessor, responseType); } @Override - public CompletableFuture convertSendAndReceiveAsType(String routingKey, Object object, @Nullable MessagePostProcessor messagePostProcessor, @Nullable ParameterizedTypeReference responseType) { - throw new UnsupportedOperationException(); + public CompletableFuture convertSendAndReceiveAsType(String queue, Object object, + @Nullable MessagePostProcessor messagePostProcessor, @Nullable ParameterizedTypeReference responseType) { + + return doConvertSendAndReceive(null, null, queue, object, messagePostProcessor, responseType); } @Override - public CompletableFuture convertSendAndReceiveAsType(String exchange, String routingKey, Object object, @Nullable MessagePostProcessor messagePostProcessor, @Nullable ParameterizedTypeReference responseType) { - throw new UnsupportedOperationException(); + public CompletableFuture convertSendAndReceiveAsType(String exchange, @Nullable String routingKey, + Object object, @Nullable MessagePostProcessor messagePostProcessor, + @Nullable ParameterizedTypeReference responseType) { + + return doConvertSendAndReceive(exchange, routingKey != null ? routingKey : this.defaultRoutingKey, null, + object, messagePostProcessor, responseType); + } + + private CompletableFuture doConvertSendAndReceive(@Nullable String exchange, @Nullable String routingKey, + @Nullable String queue, Object data, @Nullable MessagePostProcessor messagePostProcessor, + @Nullable ParameterizedTypeReference responseType) { + + Message message = convertToMessageIfNecessary(data); + if (messagePostProcessor != null) { + message = messagePostProcessor.postProcessMessage(message); + } + return doSendAndReceive(exchange, routingKey, queue, message) + .thenApply((reply) -> convertReply(reply, responseType)); + } + + private static com.rabbitmq.client.amqp.Message toAmqpMessage(@Nullable String exchange, + @Nullable String routingKey, @Nullable String queue, Message message, + Supplier amqpMessageSupplier) { + + com.rabbitmq.client.amqp.Message.MessageAddressBuilder address = + amqpMessageSupplier.get() + .toAddress(); + + JavaUtils.INSTANCE + .acceptIfNotNull(exchange, address::exchange) + .acceptIfNotNull(routingKey, address::key) + .acceptIfNotNull(queue, address::queue); + + com.rabbitmq.client.amqp.Message amqpMessage = address.message(); + + RabbitAmqpUtils.toAmqpMessage(message, amqpMessage); + + return amqpMessage; } } diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpUtils.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpUtils.java index 340b82d930..44e1df5974 100644 --- a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpUtils.java +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpUtils.java @@ -53,7 +53,8 @@ public static Message fromAmqpMessage(com.rabbitmq.client.amqp.Message amqpMessa .acceptIfNotNull(amqpMessage.contentEncoding(), messageProperties::setContentEncoding) .acceptIfNotNull(amqpMessage.absoluteExpiryTime(), (exp) -> messageProperties.setExpiration(Long.toString(exp))) - .acceptIfNotNull(amqpMessage.creationTime(), (time) -> messageProperties.setTimestamp(new Date(time))); + .acceptIfNotNull(amqpMessage.creationTime(), (time) -> messageProperties.setTimestamp(new Date(time))) + .acceptIfNotNull(amqpMessage.replyTo(), messageProperties::setReplyTo); amqpMessage.forEachProperty(messageProperties::setHeader); diff --git a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplateTests.java b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplateTests.java index afd021cdb4..7cb211c6fb 100644 --- a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplateTests.java +++ b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplateTests.java @@ -16,20 +16,31 @@ package org.springframework.amqp.rabbitmq.client; +import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.amqp.AmqpIllegalStateException; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.DirectExchange; import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageBuilder; import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.QueueBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.test.context.ContextConfiguration; +import org.springframework.util.MimeTypeUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -58,11 +69,13 @@ void tearDown() { void illegalStateOnNoDefaults() { assertThatIllegalStateException() .isThrownBy(() -> this.template.send(new Message(new byte[0]))) - .withMessage("For send with defaults, an 'exchange' (and optional 'key') or 'queue' must be provided"); + .withMessage( + "For send with defaults, an 'exchange' (and optional 'routingKey') or 'queue' must be provided"); assertThatIllegalStateException() .isThrownBy(() -> this.template.convertAndSend(new byte[0])) - .withMessage("For send with defaults, an 'exchange' (and optional 'key') or 'queue' must be provided"); + .withMessage( + "For send with defaults, an 'exchange' (and optional 'routingKey') or 'queue' must be provided"); } @Test @@ -81,7 +94,7 @@ void defaultExchangeAndRoutingKey() { @Test void defaultQueues() { this.rabbitAmqpTemplate.setQueue("q1"); - this.rabbitAmqpTemplate.setDefaultReceiveQueue("q1"); + this.rabbitAmqpTemplate.setReceiveQueue("q1"); assertThat(this.rabbitAmqpTemplate.convertAndSend("test2")) .succeedsWithin(Duration.ofSeconds(10)); @@ -91,6 +104,65 @@ void defaultQueues() { .isEqualTo("test2"); } + @Test + void verifyRpc() { + String testRequest = "rpc-request"; + String testReply = "rpc-reply"; + + CompletableFuture rpcClientResult = this.template.convertSendAndReceive("e1", "k1", testRequest); + + AtomicReference receivedRequest = new AtomicReference<>(); + CompletableFuture rpcServerResult = + this.rabbitAmqpTemplate.receiveAndReply("q1", + payload -> { + receivedRequest.set(payload); + return testReply; + }); + + assertThat(rpcServerResult).succeedsWithin(Duration.ofSeconds(10)).isEqualTo(true); + assertThat(rpcClientResult).succeedsWithin(Duration.ofSeconds(10)).isEqualTo(testReply); + assertThat(receivedRequest.get()).isEqualTo(testRequest); + + this.template.send("q1", + MessageBuilder.withBody("non-rpc-request".getBytes(StandardCharsets.UTF_8)) + .setMessageId(UUID.randomUUID().toString()) + .setContentType(MimeTypeUtils.TEXT_PLAIN_VALUE) + .build()); + + rpcServerResult = this.rabbitAmqpTemplate.receiveAndReply("q1", payload -> "reply-attempt"); + + assertThat(rpcServerResult).failsWithin(Duration.ofSeconds(10)) + .withThrowableOfType(ExecutionException.class) + .withCauseInstanceOf(AmqpIllegalStateException.class) + .withRootCauseInstanceOf(IllegalArgumentException.class) + .withMessageContaining("Failed to process RPC request: (Body:'non-rpc-request'") + .withStackTraceContaining("The 'reply-to' property has to be set on request. Used for reply publishing."); + + rpcClientResult = this.template.convertSendAndReceive("q1", testRequest); + rpcServerResult = this.rabbitAmqpTemplate.receiveAndReply("q1", payload -> null); + + assertThat(rpcServerResult).succeedsWithin(Duration.ofSeconds(10)).isEqualTo(false); + assertThat(rpcClientResult).failsWithin(Duration.ofSeconds(2)) + .withThrowableThat() + .isInstanceOf(TimeoutException.class); + + this.template.convertSendAndReceive("q1", new byte[0]); + + rpcServerResult = this.rabbitAmqpTemplate.receiveAndReply("q1", payload -> payload); + assertThat(rpcServerResult).failsWithin(Duration.ofSeconds(10)) + .withThrowableOfType(ExecutionException.class) + .withCauseInstanceOf(AmqpIllegalStateException.class) + .withRootCauseInstanceOf(ClassCastException.class) + .withMessageContaining("Failed to process RPC request: (Body:'[B") + .withStackTraceContaining("class [B cannot be cast to class java.lang.String"); + + assertThat(this.template.receiveAndConvert("dlq1")).succeedsWithin(10, TimeUnit.SECONDS) + .isEqualTo("non-rpc-request"); + + assertThat(this.template.receiveAndConvert("dlq1")).succeedsWithin(10, TimeUnit.SECONDS) + .isEqualTo(new byte[0]); + } + @Configuration static class Config { @@ -101,7 +173,7 @@ DirectExchange e1() { @Bean Queue q1() { - return new Queue("q1"); + return QueueBuilder.durable("q1").deadLetterExchange("dlx1").build(); } @Bean diff --git a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTestBase.java b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTestBase.java index 5d580e8403..d4ccbe0453 100644 --- a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTestBase.java +++ b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTestBase.java @@ -23,10 +23,13 @@ import com.rabbitmq.client.amqp.Environment; import com.rabbitmq.client.amqp.impl.AmqpEnvironmentBuilder; +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.Declarable; import org.springframework.amqp.core.Declarables; import org.springframework.amqp.core.Exchange; import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.TopicExchange; import org.springframework.amqp.rabbit.junit.AbstractTestContainerTests; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.Lifecycle; @@ -94,6 +97,21 @@ RabbitAmqpTemplate rabbitTemplate(AmqpConnectionFactory connectionFactory) { return new RabbitAmqpTemplate(connectionFactory); } + @Bean + TopicExchange dlx1() { + return new TopicExchange("dlx1"); + } + + @Bean + Queue dlq1() { + return new Queue("dlq1"); + } + + @Bean + Binding dlq1Binding() { + return BindingBuilder.bind(dlq1()).to(dlx1()).with("#"); + } + volatile boolean running; @Override diff --git a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java index 9a37503496..bbada75307 100644 --- a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java +++ b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java @@ -30,11 +30,8 @@ import org.junit.jupiter.api.Test; import org.springframework.amqp.core.AmqpAcknowledgment; -import org.springframework.amqp.core.Binding; -import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.Queue; import org.springframework.amqp.core.QueueBuilder; -import org.springframework.amqp.core.TopicExchange; import org.springframework.amqp.rabbit.annotation.EnableRabbit; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.rabbit.annotation.RabbitListenerAnnotationBeanPostProcessor; @@ -149,21 +146,6 @@ void verifyBatchConsumedAfterScheduledTimeout() { @EnableRabbit static class Config { - @Bean - TopicExchange dlx1() { - return new TopicExchange("dlx1"); - } - - @Bean - Queue dlq1() { - return new Queue("dlq1"); - } - - @Bean - Binding dlq1Binding() { - return BindingBuilder.bind(dlq1()).to(dlx1()).with("#"); - } - @Bean Queue q1() { return QueueBuilder.durable("q1").deadLetterExchange("dlx1").build(); diff --git a/spring-rabbitmq-client/src/test/resources/log4j2-test.xml b/spring-rabbitmq-client/src/test/resources/log4j2-test.xml index a4931c982e..752714e330 100644 --- a/spring-rabbitmq-client/src/test/resources/log4j2-test.xml +++ b/spring-rabbitmq-client/src/test/resources/log4j2-test.xml @@ -7,6 +7,7 @@ + From 9c0c4e28539ad506b2a49836637f4ad6c25948bc Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 6 Mar 2025 15:59:55 -0500 Subject: [PATCH 707/737] Fix Checkstyle violation in the Javadoc --- .../java/org/springframework/amqp/core/AsyncAmqpTemplate.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate.java b/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate.java index 90dfe728c4..7e8c00fe5f 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/AsyncAmqpTemplate.java @@ -109,11 +109,11 @@ default CompletableFuture receiveAndReply(ReceiveAndReplyCallbac * The request message must have a {@code replyTo} property. * The request {@code messageId} property is used for correlation. * The callback might not produce a reply with the meaning nothing to answer. + * @param the request body type. + * @param the response body type * @param queueName the queue to consume request. * @param callback an application callback to handle request and produce reply. * @return the completion status: true if no errors and reply has been produced. - * @param the request body type. - * @param the response body type */ default CompletableFuture receiveAndReply(String queueName, ReceiveAndReplyCallback callback) { throw new UnsupportedOperationException(); From 994df8851ab2300edf9917d771a53c63cdce2332 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 7 Mar 2025 11:44:57 -0500 Subject: [PATCH 708/737] Fix race condition in the `RabbitAmqpListenerTests` According to the `Collections.synchronizedList()` Javadocs, it has to be iterated with a `synchronized (list)`. Otherwise, there is no guarantee that memory barrier for list items is fulfilled. --- .../rabbitmq/client/listener/RabbitAmqpListenerTests.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java index bbada75307..816756fd92 100644 --- a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java +++ b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java @@ -89,7 +89,9 @@ void verifyAllDataIsConsumedFromQ1AndQ2() throws InterruptedException { assertThat(this.config.consumeIsDone.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(this.config.received).containsAll(testDataList); + synchronized (this.config.received) { + assertThat(this.config.received).containsAll(testDataList); + } assertThat(this.template.receive("dlq1")).succeedsWithin(10, TimeUnit.SECONDS); assertThat(this.template.receive("dlq1")).succeedsWithin(10, TimeUnit.SECONDS); @@ -166,7 +168,7 @@ RabbitAmqpListenerContainerFactory rabbitAmqpListenerContainerFactory(AmqpConnec return new RabbitAmqpListenerContainerFactory(connectionFactory); } - List received = Collections.synchronizedList(new ArrayList<>()); + final List received = Collections.synchronizedList(new ArrayList<>()); CountDownLatch consumeIsDone = new CountDownLatch(10); From 355be1ee4c20dd1179c6eed59bb43fac5d18db6b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 8 Mar 2025 02:48:45 +0000 Subject: [PATCH 709/737] Bump org.testcontainers:testcontainers-bom from 1.20.5 to 1.20.6 (#3003) Bumps [org.testcontainers:testcontainers-bom](https://github.com/testcontainers/testcontainers-java) from 1.20.5 to 1.20.6. - [Release notes](https://github.com/testcontainers/testcontainers-java/releases) - [Changelog](https://github.com/testcontainers/testcontainers-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/testcontainers/testcontainers-java/compare/1.20.5...1.20.6) --- updated-dependencies: - dependency-name: org.testcontainers:testcontainers-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f02e407c7c..3aaf951c20 100644 --- a/build.gradle +++ b/build.gradle @@ -66,7 +66,7 @@ ext { springDataVersion = '2025.1.0-SNAPSHOT' springRetryVersion = '2.0.11' springVersion = '7.0.0-SNAPSHOT' - testcontainersVersion = '1.20.5' + testcontainersVersion = '1.20.6' javaProjects = subprojects - project(':spring-amqp-bom') } From b6b61c4cfe2ce6b6ee8c1639bd11eaa3662f3cba Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 10 Mar 2025 10:29:53 -0400 Subject: [PATCH 710/737] Improve `RabbitAmqpUtils.toAmqpMessage()` The `RabbitAmqpUtils.toAmqpMessage()` utility is used on the publisher side, so, it is natural to treat such a message as a reply. Therefore, the `correlationId` is set to `messageId` of `correlationId` is not present. The `replyTo` of the Spring message is set into `to` of the AMQP message * Mention in the `Address` JavaDocs that just `routingKey` can be treated differently by clients * Fix error message in the `RabbitAmqpMessageListenerAdapter` --- .../org/springframework/amqp/core/Address.java | 15 +++++++++------ .../amqp/rabbitmq/client/RabbitAmqpUtils.java | 15 +++++++++++---- .../RabbitAmqpMessageListenerAdapter.java | 2 +- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/Address.java b/spring-amqp/src/main/java/org/springframework/amqp/core/Address.java index 564c752d16..36a940e5f8 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/Address.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/Address.java @@ -23,8 +23,8 @@ import org.springframework.util.StringUtils; /** - * Represents an address for publication of an AMQP message. The AMQP 0-8 and 0-9 - * specifications have an unstructured string that is used as a "reply to" address. + * Represents an address for publication of an AMQP message. The AMQP 0.9 + * specification has an unstructured string that is used as a "reply to" address. * There are however conventions in use and this class makes it easier to * follow these conventions, which can be easily summarised as: * @@ -33,7 +33,10 @@ * * * Here we also the exchange name to default to empty - * (so just a routing key will work if you know the queue name). + * (so just a routing key will work as a queue name). + *

+ * For AMQP 1.0, only routing key is treated as target destination. + * * * @author Mark Pollack * @author Mark Fisher @@ -58,11 +61,11 @@ public class Address { /** * Create an Address instance from a structured String with the form - * *

 	 * (exchange)/(routingKey)
 	 * 
* . + * If exchange is parsed to empty string, then routing key is treated as a queue name. * @param address a structured string. */ public Address(String address) { @@ -120,9 +123,9 @@ public boolean equals(Object o) { @Override public int hashCode() { - int result = this.exchangeName != null ? this.exchangeName.hashCode() : 0; + int result = this.exchangeName.hashCode(); int prime = 31; // NOSONAR magic # - result = prime * result + (this.routingKey != null ? this.routingKey.hashCode() : 0); + result = prime * result + this.routingKey.hashCode(); return result; } diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpUtils.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpUtils.java index 44e1df5974..c10ebebd77 100644 --- a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpUtils.java +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpUtils.java @@ -19,6 +19,7 @@ import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.Map; +import java.util.Objects; import java.util.UUID; import com.rabbitmq.client.amqp.Consumer; @@ -72,8 +73,12 @@ public static Message fromAmqpMessage(com.rabbitmq.client.amqp.Message amqpMessa } /** - * Convert {@link com.rabbitmq.client.amqp.Message} into {@link Message}. - * @param amqpMessage the {@link com.rabbitmq.client.amqp.Message} convert from. + * Convert {@link Message} into {@link com.rabbitmq.client.amqp.Message}. + * The {@link MessageProperties#getReplyTo()} is set into {@link com.rabbitmq.client.amqp.Message#to(String)}. + * The {@link com.rabbitmq.client.amqp.Message#correlationId(long)} is set to + * {@link MessageProperties#getCorrelationId()} if present, or to {@link MessageProperties#getMessageId()}. + * @param message the {@link Message} convert from. + * @param amqpMessage the {@link com.rabbitmq.client.amqp.Message} convert into. */ public static void toAmqpMessage(Message message, com.rabbitmq.client.amqp.Message amqpMessage) { MessageProperties messageProperties = message.getMessageProperties(); @@ -83,9 +88,11 @@ public static void toAmqpMessage(Message message, com.rabbitmq.client.amqp.Messa .contentEncoding(messageProperties.getContentEncoding()) .contentType(messageProperties.getContentType()) .messageId(messageProperties.getMessageId()) - .correlationId(messageProperties.getCorrelationId()) + .correlationId( + Objects.requireNonNullElse( + messageProperties.getCorrelationId(), messageProperties.getMessageId())) .priority(messageProperties.getPriority().byteValue()) - .replyTo(messageProperties.getReplyTo()); + .to(messageProperties.getReplyTo()); Map headers = messageProperties.getHeaders(); if (!headers.isEmpty()) { diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListenerAdapter.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListenerAdapter.java index a94e2f88aa..0bd49bf67a 100644 --- a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListenerAdapter.java +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListenerAdapter.java @@ -120,7 +120,7 @@ public void onMessageBatch(List messages) { InvocationResult result = getHandlerAdapter() .invoke(converted, amqpAcknowledgment); if (result.getReturnValue() != null) { - logger.warn("Replies are not currently supported with RabbitMQ AMQP 1.0 listeners"); + logger.warn("Replies for batches are not currently supported with RabbitMQ AMQP 1.0 listeners"); } } catch (Exception ex) { From 6b2ad0dc7341df8ac4f2a147fe87c9f524f34a9a Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 10 Mar 2025 11:17:07 -0400 Subject: [PATCH 711/737] Fix `RabbitAmqpUtils.toAmqpMessage()` util for `to` prop The `messageProperties.getReplyTo()` might be null, so set it into `amqpMessage::to` only if it is not null * Add convenient `JavaUtils.acceptOrElseIfNotNull()` for two alternative values --- .../springframework/amqp/utils/JavaUtils.java | 20 +++++++++++++++++++ .../amqp/rabbitmq/client/RabbitAmqpUtils.java | 10 ++++------ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/utils/JavaUtils.java b/spring-amqp/src/main/java/org/springframework/amqp/utils/JavaUtils.java index ee15135324..1a1a31af2f 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/utils/JavaUtils.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/utils/JavaUtils.java @@ -136,4 +136,24 @@ public JavaUtils acceptIfHasText(T t1, @Nullable String value, BiConsumer the value type. + * @return this. + * @since 4.0 + */ + public JavaUtils acceptOrElseIfNotNull(@Nullable T value, @Nullable T alternative, Consumer consumer) { + if (value != null) { + consumer.accept(value); + } + else if (alternative != null) { + consumer.accept(alternative); + } + return this; + } + } diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpUtils.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpUtils.java index c10ebebd77..f19a865947 100644 --- a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpUtils.java +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpUtils.java @@ -19,7 +19,6 @@ import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.Map; -import java.util.Objects; import java.util.UUID; import com.rabbitmq.client.amqp.Consumer; @@ -88,11 +87,7 @@ public static void toAmqpMessage(Message message, com.rabbitmq.client.amqp.Messa .contentEncoding(messageProperties.getContentEncoding()) .contentType(messageProperties.getContentType()) .messageId(messageProperties.getMessageId()) - .correlationId( - Objects.requireNonNullElse( - messageProperties.getCorrelationId(), messageProperties.getMessageId())) - .priority(messageProperties.getPriority().byteValue()) - .to(messageProperties.getReplyTo()); + .priority(messageProperties.getPriority().byteValue()); Map headers = messageProperties.getHeaders(); if (!headers.isEmpty()) { @@ -100,8 +95,11 @@ public static void toAmqpMessage(Message message, com.rabbitmq.client.amqp.Messa } JavaUtils.INSTANCE + .acceptOrElseIfNotNull(messageProperties.getCorrelationId(), + messageProperties.getMessageId(), amqpMessage::correlationId) .acceptIfNotNull(messageProperties.getUserId(), (userId) -> amqpMessage.userId(userId.getBytes(StandardCharsets.UTF_8))) + .acceptIfNotNull(messageProperties.getReplyTo(), amqpMessage::to) .acceptIfNotNull(messageProperties.getTimestamp(), (timestamp) -> amqpMessage.creationTime(timestamp.getTime())) .acceptIfNotNull(messageProperties.getExpiration(), From b55010f7423ac948c7b0d8ea92060bb0b6c86c41 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 10 Mar 2025 12:17:53 -0400 Subject: [PATCH 712/737] GH-3008: Fix `SimpleMLC.logConsumerException` for `warn` Fixes: #3008 Issue link: https://github.com/spring-projects/spring-amqp/issues/3008 The real problem in consumer (e.g. `ClassCastException` mentioned in the issue) might be lost if DEBUG logging level is not enabled * Fix the first condition in the `SimpleMessageListenerContainer.logConsumerException()` to use `logger.warn()` as it was before https://github.com/spring-projects/spring-amqp/issues/2278 fix **Auto-cherry-pick to `3.2.x` & `3.1.x`** --- .../amqp/rabbit/listener/SimpleMessageListenerContainer.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java index 4902c16e3f..89f09d2de2 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java @@ -1601,7 +1601,9 @@ private void killOrRestart(boolean aborted) { private void logConsumerException(Throwable t) { if (logger.isDebugEnabled() || !(t instanceof AmqpConnectException || t instanceof ConsumerCancelledException)) { - logger.debug( + // It has to be WARN independently of condition. + // The meaning is: log WARN for all exception when DEBUG enabled, or all others, but mentioned + logger.warn( "Consumer raised exception, processing can restart if the connection factory supports it", t); } From 30efbf7a5b5198f9494c659d817537d65657085f Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 10 Mar 2025 14:20:22 -0400 Subject: [PATCH 713/737] GH-3005: Fix `SimpleMLC.killOrRestart` for closed AC Fixes: #3005 Issue link: https://github.com/spring-projects/spring-amqp/issues/3005 The `SimpleMessageListenerContainer$AsyncMessageProcessingConsumer.killOrRestart()` is also called during application context shutdown. At this moment we cannot emit events into an application context. Otherwise, it fails with: ``` Exception in thread "rabbitListenerExecutor1" org.springframework.beans.factory.BeanCreationNotAllowedException: Error creating bean with name 'refreshEventListener': Singleton bean creation not allowed while singletons of this factory are in destruction (Do not request a bean from a BeanFactory in a destroy method implementation!) ``` * Introduce `ObservableListenerContainer.isApplicationContextClosed()` and call it as additional condition in the `SimpleMessageListenerContainer$AsyncMessageProcessingConsumer.killOrRestart()` before trying to emit `AsyncConsumerStoppedEvent` **Auto-cherry-pick to `3.2.x`** The fix for `3.1.x` requires a slightly different approach via `ContextClosedEvent` --- .../rabbit/listener/ObservableListenerContainer.java | 7 +++++++ .../rabbit/listener/SimpleMessageListenerContainer.java | 9 ++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ObservableListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ObservableListenerContainer.java index 21c04d5ab0..c8b2925c75 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ObservableListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/ObservableListenerContainer.java @@ -26,6 +26,7 @@ import org.springframework.beans.factory.DisposableBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.util.ClassUtils; /** @@ -121,6 +122,12 @@ protected void checkObservation() { } } + + protected boolean isApplicationContextClosed() { + return this.applicationContext instanceof ConfigurableApplicationContext configurableCtx + && configurableCtx.isClosed(); + } + @Override public void setBeanName(String beanName) { this.beanName = beanName; diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java index 89f09d2de2..d074e5e98b 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/SimpleMessageListenerContainer.java @@ -64,6 +64,7 @@ import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.amqp.support.ConsumerTagStrategy; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.core.log.LogMessage; import org.springframework.jmx.export.annotation.ManagedMetric; import org.springframework.jmx.support.MetricType; @@ -807,7 +808,6 @@ protected void adjustConsumers(int deltaArg) { } } - /** * Start up to delta consumers, limited by {@link #setMaxConcurrentConsumers(int)}. * @param delta the consumers to add. @@ -875,7 +875,6 @@ private void considerAddingAConsumer() { } } - private void considerStoppingAConsumer(BlockingQueueConsumer consumer) { this.consumersLock.lock(); try { @@ -1272,7 +1271,6 @@ private final class AsyncMessageProcessingConsumer implements Runnable { private boolean failedExclusive; - AsyncMessageProcessingConsumer(BlockingQueueConsumer consumer) { this.consumer = consumer; this.start = new CountDownLatch(1); @@ -1556,8 +1554,9 @@ private void killOrRestart(boolean aborted) { try { this.consumer.stop(); SimpleMessageListenerContainer.this.cancellationLock.release(this.consumer); - if (getApplicationEventPublisher() != null) { - getApplicationEventPublisher().publishEvent( + ApplicationEventPublisher applicationEventPublisher = getApplicationEventPublisher(); + if (applicationEventPublisher != null && !isApplicationContextClosed()) { + applicationEventPublisher.publishEvent( new AsyncConsumerStoppedEvent(SimpleMessageListenerContainer.this, this.consumer)); } } From 3ecb055edad96bbef1c1e5a203e7ffef398005de Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Tue, 11 Mar 2025 11:05:02 -0400 Subject: [PATCH 714/737] Fix bug in the `RabbitAmqpListenerTests` for `consumeIsDone` The logic if the `processQ1AndQ2Data()` listener method is to collect 10 messages, where one of them is a duplication after `requeue`. The first `discard` is accepted into `received` result, but the second is terminated by the exception. However, `countDown()` in the `finally` block is still called for all the cases. Therefore, `11` times: 7 for normal data, 1 for `discard`, 2 for `requeue`, and 1 for exception. --- .../amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java index 816756fd92..7d286157a6 100644 --- a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java +++ b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java @@ -170,7 +170,7 @@ RabbitAmqpListenerContainerFactory rabbitAmqpListenerContainerFactory(AmqpConnec final List received = Collections.synchronizedList(new ArrayList<>()); - CountDownLatch consumeIsDone = new CountDownLatch(10); + CountDownLatch consumeIsDone = new CountDownLatch(11); @RabbitListener(queues = {"q1", "q2"}, ackMode = "#{T(org.springframework.amqp.core.AcknowledgeMode).MANUAL}", From 1741803975301b02e466d0f173e7d1406bc7d3f3 Mon Sep 17 00:00:00 2001 From: Raul Avila <87972502+raul-avila-ph@users.noreply.github.com> Date: Wed, 12 Mar 2025 14:43:20 +0000 Subject: [PATCH 715/737] GH-3006: Read x-death count in a safer way Fixes: #3006 We had an issue in our system trying to consume a message from RabbitMQ that contained an `x-death` header, with count value typed as `Integer`. This caused a `ClassCastException`. The reason the count value was an int and not a long is that we were storing headers in an internal database as part of a recovery process, and the typing was slightly changed during serialisation / deserialisation. * Use `target.setRetryCount(numberValue.longValue());` in the `DefaultMessagePropertiesConverter` instead of cast to `long` Signed-off-by: Raul Avila [artem.bilan@broadcom.com Improve commit message] **Auto-cherry-pick to `3.2.x`** Signed-off-by: Artem Bilan --- .../DefaultMessagePropertiesConverter.java | 7 ++++++- .../DefaultMessagePropertiesConverterTests.java | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java index 0d9e3cfd58..728b43e9f9 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverter.java @@ -44,6 +44,7 @@ * @author Artem Bilan * @author Ngoc Nhan * @author Johan Kaving + * @author Raul Avila * * @since 1.0 */ @@ -147,7 +148,11 @@ else if (MessageProperties.RETRY_COUNT.equals(key)) { if (target.getRetryCount() == 0) { List> xDeathHeader = target.getXDeathHeader(); if (!CollectionUtils.isEmpty(xDeathHeader)) { - target.setRetryCount((long) xDeathHeader.get(0).get("count")); + Object value = xDeathHeader.get(0).get("count"); + + if (value instanceof Number numberValue) { + target.setRetryCount(numberValue.longValue()); + } } } diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverterTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverterTests.java index 91ce11892f..dd2cc1eab7 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverterTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/DefaultMessagePropertiesConverterTests.java @@ -104,6 +104,21 @@ public void testToMessagePropertiesLongStringInMap() { assertThat(((Map) messageProperties.getHeaders().get("map")).get("longString")).as("LongString nested in Map not converted to String").isEqualTo(longStringString); } + @Test + public void testToMessagePropertiesXDeathCount() { + Map headers = new HashMap(); + + headers.put("x-death", List.of(Map.of("count", Integer.valueOf(2)))); + + BasicProperties source = new BasicProperties.Builder() + .headers(headers) + .build(); + + MessageProperties messageProperties = messagePropertiesConverter.toMessageProperties(source, envelope, "UTF-8"); + + assertThat(messageProperties.getRetryCount()).isEqualTo(2); + } + @Test public void testLongLongString() { Map headers = new HashMap(); From ca1174f7d1db9ce1aa6be7e2aee0d31270950a17 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 13 Mar 2025 11:58:54 -0400 Subject: [PATCH 716/737] GH-3014: Add request/reply support into `RabbitAmqpMessageListenerAdapter` Fixes: https://github.com/spring-projects/spring-amqp/issues/3014 --- build.gradle | 1 + .../AbstractAdaptableMessageListener.java | 25 +++--- .../MessagingMessageListenerAdapter.java | 6 +- .../adapter/MessageListenerAdapterTests.java | 38 ++++++--- .../listener/RabbitAmqpListenerContainer.java | 11 +++ .../RabbitAmqpMessageListenerAdapter.java | 84 ++++++++++++++++++- .../listener/RabbitAmqpListenerTests.java | 69 +++++++++++++++ 7 files changed, 208 insertions(+), 26 deletions(-) diff --git a/build.gradle b/build.gradle index 3aaf951c20..70a584d593 100644 --- a/build.gradle +++ b/build.gradle @@ -481,6 +481,7 @@ project('spring-rabbitmq-client') { api "com.rabbitmq.client:amqp-client:$rabbitmqAmqpClientVersion" testApi project(':spring-rabbit-junit') + testApi 'io.projectreactor:reactor-core' testRuntimeOnly 'com.fasterxml.jackson.core:jackson-databind' diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java index f49266e05d..88e395d67c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java @@ -216,6 +216,10 @@ public void setBeforeSendReplyPostProcessors(MessagePostProcessor... beforeSendR beforeSendReplyPostProcessors.length); } + public MessagePostProcessor @Nullable [] getBeforeSendReplyPostProcessors() { + return this.beforeSendReplyPostProcessors; + } + /** * Set a {@link RetryTemplate} to use when sending replies. * @param retryTemplate the template. @@ -369,7 +373,7 @@ protected void handleResult(InvocationResult resultArg, Message request, @Nullab /** * Handle the given result object returned from the listener method, sending a * response message back. - * @param resultArg the result object to handle (never null) + * @param resultArg the result object to handle * @param request the original request message * @param channel the Rabbit channel to operate on (maybe null) * @param source the source data for the method invocation - e.g. @@ -383,7 +387,7 @@ protected void handleResult(InvocationResult resultArg, Message request, @Nullab protected void handleResult(@Nullable InvocationResult resultArg, Message request, @Nullable Channel channel, @Nullable Object source) { - if (channel != null && resultArg != null) { + if (resultArg != null) { if (resultArg.getReturnValue() instanceof CompletableFuture completable) { if (!this.isManualAck) { this.logger.warn("Container AcknowledgeMode must be MANUAL for a Future return type; " @@ -413,13 +417,9 @@ else if (monoPresent && MonoHandler.isMono(resultArg.getReturnValue())) { doHandleResult(resultArg, request, channel, source); } } - else if (this.logger.isWarnEnabled()) { - this.logger.warn("Listener method returned result [" + resultArg - + "]: not generating response message for it because no Rabbit Channel given"); - } } - private void asyncSuccess(InvocationResult resultArg, Message request, Channel channel, + private void asyncSuccess(InvocationResult resultArg, Message request, @Nullable Channel channel, @Nullable Object source, @Nullable Object deferredResult) { if (deferredResult == null) { @@ -458,8 +458,9 @@ protected void basicAck(Message request, @Nullable Channel channel) { } } - protected void asyncFailure(Message request, Channel channel, Throwable t, @Nullable Object source) { + protected void asyncFailure(Message request, @Nullable Channel channel, Throwable t, @Nullable Object source) { this.logger.error("Future, Mono, or suspend function was completed with an exception for " + request, t); + Assert.notNull(channel, "'channel' must not be null."); try { channel.basicNack(request.getMessageProperties().getDeliveryTag(), false, ContainerUtils.shouldRequeue(this.defaultRequeueRejected, t, this.logger)); @@ -469,7 +470,7 @@ protected void asyncFailure(Message request, Channel channel, Throwable t, @Null } } - protected void doHandleResult(InvocationResult resultArg, Message request, Channel channel, + protected void doHandleResult(InvocationResult resultArg, Message request, @Nullable Channel channel, @Nullable Object source) { if (this.logger.isDebugEnabled()) { @@ -500,12 +501,13 @@ protected void doHandleResult(InvocationResult resultArg, Message request, Chann /** * Build a Rabbit message to be sent as response based on the given result object. * @param channel the Rabbit Channel to operate on. + * Can be null if implementation does not support AMQP 0.9.1. * @param result the content of the message, as returned from the listener method. * @param genericType the generic type to populate type headers. * @return the Rabbit Message (never null). * @see #setMessageConverter */ - protected Message buildMessage(Channel channel, @Nullable Object result, @Nullable Type genericType) { + protected Message buildMessage(@Nullable Channel channel, @Nullable Object result, @Nullable Type genericType) { MessageConverter converter = getMessageConverter(); if (converter != null && !(result instanceof Message)) { return convert(result, genericType, converter); @@ -633,7 +635,8 @@ private Address evaluateReplyTo(Message request, @Nullable Object source, @Nulla * @see #postProcessResponse(Message, Message) * @see #setReplyPostProcessor(ReplyPostProcessor) */ - protected void sendResponse(Channel channel, Address replyTo, Message messageIn) { + protected void sendResponse(@Nullable Channel channel, Address replyTo, Message messageIn) { + Assert.notNull(channel, "'channel' must not be null."); Message message = messageIn; if (this.beforeSendReplyPostProcessors != null) { for (MessagePostProcessor postProcessor : this.beforeSendReplyPostProcessors) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java index a0a99834a4..20ce47ea34 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java @@ -169,7 +169,7 @@ public void onMessage(org.springframework.amqp.core.Message amqpMessage, @Nullab } @Override - protected void asyncFailure(org.springframework.amqp.core.Message request, Channel channel, Throwable t, + protected void asyncFailure(org.springframework.amqp.core.Message request, @Nullable Channel channel, Throwable t, @Nullable Object source) { try { @@ -183,7 +183,7 @@ protected void asyncFailure(org.springframework.amqp.core.Message request, Chann super.asyncFailure(request, channel, t, source); } - private void handleException(org.springframework.amqp.core.Message amqpMessage, @Nullable Channel channel, + protected void handleException(org.springframework.amqp.core.Message amqpMessage, @Nullable Channel channel, @Nullable Message message, ListenerExecutionFailedException e) throws Exception { // NOSONAR if (this.errorHandler != null) { @@ -307,7 +307,7 @@ private String createMessagingErrorMessage(Object payload) { * @see #setMessageConverter */ @Override - protected org.springframework.amqp.core.Message buildMessage(Channel channel, @Nullable Object result, + protected org.springframework.amqp.core.Message buildMessage(@Nullable Channel channel, @Nullable Object result, @Nullable Type genericType) { MessageConverter converter = getMessageConverter(); diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapterTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapterTests.java index 58b1c5d942..4fc1d3c433 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapterTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/listener/adapter/MessageListenerAdapterTests.java @@ -24,6 +24,7 @@ import java.util.concurrent.atomic.AtomicReference; import com.rabbitmq.client.Channel; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; @@ -33,7 +34,6 @@ import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.support.SendRetryContextAccessor; -import org.springframework.amqp.support.converter.SimpleMessageConverter; import org.springframework.aop.framework.ProxyFactory; import org.springframework.retry.RetryPolicy; import org.springframework.retry.policy.SimpleRetryPolicy; @@ -67,8 +67,15 @@ public class MessageListenerAdapterTests { public void init() { this.messageProperties = new MessageProperties(); this.messageProperties.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN); - this.adapter = new MessageListenerAdapter(); - this.adapter.setMessageConverter(new SimpleMessageConverter()); + this.adapter = new MessageListenerAdapter() { + + @Override + protected void doHandleResult(InvocationResult resultArg, Message request, @Nullable Channel channel, + @Nullable Object source) { + + } + + }; } @Test @@ -77,7 +84,7 @@ class ExtendedListenerAdapter extends MessageListenerAdapter { @Override protected Object[] buildListenerArguments(Object extractedMessage, Channel channel, Message message) { - return new Object[] { extractedMessage, channel, message }; + return new Object[] {extractedMessage, channel, message}; } } @@ -131,7 +138,15 @@ public String myPojoMessageMethod(String input) { } } - this.adapter = new MessageListenerAdapter(new Delegate(), "myPojoMessageMethod"); + this.adapter = new MessageListenerAdapter(new Delegate(), "myPojoMessageMethod") { + + @Override + protected void doHandleResult(InvocationResult resultArg, Message request, @Nullable Channel channel, + @Nullable Object source) { + + } + + }; this.adapter.onMessage(new Message("foo".getBytes(), messageProperties), null); assertThat(called.get()).isTrue(); } @@ -146,7 +161,7 @@ public void testExplicitListenerMethod() throws Exception { @Test public void testMappedListenerMethod() throws Exception { - Map map = new HashMap(); + Map map = new HashMap<>(); map.put("foo", "handle"); map.put("bar", "notDefinedOnInterface"); this.adapter.setDefaultListenerMethod("anotherHandle"); @@ -186,6 +201,7 @@ public void testJdkProxyListener() throws Exception { @Test public void testReplyRetry() throws Exception { + this.adapter = new MessageListenerAdapter(); this.adapter.setDefaultListenerMethod("handle"); this.adapter.setDelegate(this.simpleService); RetryPolicy retryPolicy = new SimpleRetryPolicy(2); @@ -210,7 +226,7 @@ public void testReplyRetry() throws Exception { this.adapter.onMessage(message, channel); assertThat(this.simpleService.called).isEqualTo("handle"); assertThat(replyMessage.get()).isNotNull(); - assertThat(new String(replyMessage.get().getBody())).isEqualTo("processedfoo"); + assertThat(new String(replyMessage.get().getBody())).isEqualTo("processed foo"); assertThat(replyAddress.get()).isNotNull(); assertThat(replyAddress.get().getExchangeName()).isEqualTo("foo"); assertThat(replyAddress.get().getRoutingKey()).isEqualTo("bar"); @@ -224,7 +240,7 @@ class Delegate { @SuppressWarnings("unused") public CompletableFuture myPojoMessageMethod(String input) { CompletableFuture future = new CompletableFuture<>(); - future.complete("processed" + input); + future.complete("processed " + input); return future; } @@ -270,18 +286,18 @@ public static class SimpleService implements Service { @Override public String handle(String input) { called = "handle"; - return "processed" + input; + return "processed " + input; } @Override public String anotherHandle(String input) { called = "anotherHandle"; - return "processed" + input; + return "processed " + input; } public String notDefinedOnInterface(String input) { called = "notDefinedOnInterface"; - return "processed" + input; + return "processed " + input; } } diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerContainer.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerContainer.java index 9ab6d8a67a..1686051fc5 100644 --- a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerContainer.java +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerContainer.java @@ -99,6 +99,8 @@ public class RabbitAmqpListenerContainer implements MessageListenerContainer, Be private @Nullable MessageListener proxy; + private boolean asyncReplies; + private ErrorHandler errorHandler = new ConditionalRejectingErrorHandler(); private @Nullable Collection afterReceivePostProcessors; @@ -255,6 +257,10 @@ public String getListenerId() { @Override public void setupMessageListener(MessageListener messageListener) { this.messageListener = messageListener; + this.asyncReplies = messageListener.isAsyncReplies(); + if (this.messageListener instanceof RabbitAmqpMessageListenerAdapter rabbitAmqpMessageListenerAdapter) { + rabbitAmqpMessageListenerAdapter.setConnectionFactory(this.connectionFactory); + } this.proxy = this.messageListener; if (!ObjectUtils.isEmpty(this.adviceChain)) { ProxyFactory factory = new ProxyFactory(messageListener); @@ -276,6 +282,11 @@ public void afterPropertiesSet() { Assert.state(this.queues != null, "At least one queue has to be provided for consuming."); Assert.state(this.messageListener != null, "The 'messageListener' must be provided."); + if (this.asyncReplies && this.autoSettle) { + LOG.info("Enforce MANUAL settlement for async replies."); + this.autoSettle = false; + } + this.messageListener.containerAckMode(this.autoSettle ? AcknowledgeMode.AUTO : AcknowledgeMode.MANUAL); if (this.messageListener instanceof RabbitAmqpMessageListenerAdapter adapter && this.afterReceivePostProcessors != null) { diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListenerAdapter.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListenerAdapter.java index 0bd49bf67a..123e1b403d 100644 --- a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListenerAdapter.java +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpMessageListenerAdapter.java @@ -20,19 +20,27 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.concurrent.CompletableFuture; +import com.rabbitmq.client.Channel; import com.rabbitmq.client.amqp.Consumer; import org.jspecify.annotations.Nullable; +import org.springframework.amqp.core.Address; import org.springframework.amqp.core.AmqpAcknowledgment; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessagePostProcessor; import org.springframework.amqp.rabbit.listener.adapter.InvocationResult; import org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter; import org.springframework.amqp.rabbit.listener.api.RabbitListenerErrorHandler; +import org.springframework.amqp.rabbit.listener.support.ContainerUtils; import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; +import org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; import org.springframework.amqp.rabbitmq.client.RabbitAmqpUtils; import org.springframework.messaging.support.GenericMessage; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * A {@link MessagingMessageListenerAdapter} extension for the {@link RabbitAmqpMessageListener}. @@ -44,6 +52,11 @@ *
  • {@link Consumer.Context} - RabbitMQ AMQP client consumer settlement API.
  • *
  • {@link org.springframework.amqp.core.AmqpAcknowledgment} - Spring AMQP acknowledgment abstraction: delegates to the {@link Consumer.Context}
  • * + *

    + * This class reuses the {@link MessagingMessageListenerAdapter} as much as possible just to avoid duplication. + * The {@link Channel} abstraction from AMQP Client 0.9.1 is out use and present here just for API compatibility + * and to follow DRY principle. + * Can be reworked eventually, when this AMQP 1.0 client won't be based on {@code spring-rabbit} dependency. * * @author Artem Bilan * @@ -54,6 +67,8 @@ public class RabbitAmqpMessageListenerAdapter extends MessagingMessageListenerAd private @Nullable Collection afterReceivePostProcessors; + private @Nullable RabbitAmqpTemplate rabbitAmqpTemplate; + public RabbitAmqpMessageListenerAdapter(@Nullable Object bean, @Nullable Method method, boolean returnExceptions, @Nullable RabbitListenerErrorHandler errorHandler, boolean batch) { @@ -64,6 +79,14 @@ public void setAfterReceivePostProcessors(Collection after this.afterReceivePostProcessors = new ArrayList<>(afterReceivePostProcessors); } + /** + * Set a {@link AmqpConnectionFactory} for publishing replies from this adapter. + * @param connectionFactory the {@link AmqpConnectionFactory} for replies. + */ + public void setConnectionFactory(AmqpConnectionFactory connectionFactory) { + this.rabbitAmqpTemplate = new RabbitAmqpTemplate(connectionFactory); + } + @Override public void onAmqpMessage(com.rabbitmq.client.amqp.Message amqpMessage, Consumer.@Nullable Context context) { org.springframework.amqp.core.Message springMessage = RabbitAmqpUtils.fromAmqpMessage(amqpMessage, context); @@ -78,15 +101,74 @@ public void onAmqpMessage(com.rabbitmq.client.amqp.Message amqpMessage, Consumer .invoke(messagingMessage, springMessage, springMessage.getMessageProperties().getAmqpAcknowledgment(), amqpMessage, context); + if (result.getReturnValue() != null) { - logger.warn("Replies are not currently supported with RabbitMQ AMQP 1.0 listeners"); + Assert.notNull(this.rabbitAmqpTemplate, + "The 'connectionFactory' must be provided for handling replies."); + handleResult(result, springMessage, null, messagingMessage); } + } catch (Exception ex) { throw new ListenerExecutionFailedException("Failed to invoke listener", ex, springMessage); } } + @Override + protected void asyncFailure(Message request, @Nullable Channel channel, Throwable t, @Nullable Object source) { + try { + handleException(request, channel, (org.springframework.messaging.Message) source, + new ListenerExecutionFailedException("Async Fail", t, request)); + return; + } + catch (Exception ex) { + // Ignore and reject the message against original error + } + + this.logger.error("Future, Mono, or suspend function was completed with an exception for " + request, t); + AmqpAcknowledgment amqpAcknowledgment = request.getMessageProperties().getAmqpAcknowledgment(); + Assert.notNull(amqpAcknowledgment, "'(amqpAcknowledgment' must be provided into request message."); + + if (ContainerUtils.shouldRequeue(isDefaultRequeueRejected(), t, this.logger)) { + amqpAcknowledgment.acknowledge(AmqpAcknowledgment.Status.REQUEUE); + } + else { + amqpAcknowledgment.acknowledge(AmqpAcknowledgment.Status.REJECT); + } + } + + @Override + @SuppressWarnings("NullAway") // Dataflow analysis limitation + protected void sendResponse(@Nullable Channel channel, Address replyTo, Message messageIn) { + Message replyMessage = messageIn; + MessagePostProcessor[] beforeSendReplyPostProcessors = getBeforeSendReplyPostProcessors(); + if (beforeSendReplyPostProcessors != null) { + for (MessagePostProcessor postProcessor : beforeSendReplyPostProcessors) { + replyMessage = postProcessor.postProcessMessage(replyMessage); + } + } + + String replyToExchange = replyTo.getExchangeName(); + String replyToRoutingKey = replyTo.getRoutingKey(); + CompletableFuture sendFuture; + if (StringUtils.hasText(replyToExchange)) { + sendFuture = this.rabbitAmqpTemplate.send(replyToExchange, replyToRoutingKey, replyMessage); + } + else { + Assert.hasText(replyToRoutingKey, "The 'replyTo' must be provided, in request message or in @SendTo."); + sendFuture = this.rabbitAmqpTemplate.send(replyToRoutingKey.replaceFirst("queues/", ""), replyMessage); + } + + sendFuture.join(); + } + + @Override + protected void basicAck(Message request, @Nullable Channel channel) { + AmqpAcknowledgment amqpAcknowledgment = request.getMessageProperties().getAmqpAcknowledgment(); + Assert.notNull(amqpAcknowledgment, "'(amqpAcknowledgment' must be provided into request message."); + amqpAcknowledgment.acknowledge(); + } + @Override public void onMessageBatch(List messages) { AmqpAcknowledgment amqpAcknowledgment = diff --git a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java index 7d286157a6..17d68e1752 100644 --- a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java +++ b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java @@ -28,8 +28,12 @@ import com.rabbitmq.client.amqp.Consumer; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; import org.springframework.amqp.core.AmqpAcknowledgment; +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.DirectExchange; import org.springframework.amqp.core.Queue; import org.springframework.amqp.core.QueueBuilder; import org.springframework.amqp.rabbit.annotation.EnableRabbit; @@ -45,6 +49,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.test.context.ContextConfiguration; import org.springframework.util.MultiValueMap; @@ -144,6 +149,32 @@ void verifyBatchConsumedAfterScheduledTimeout() { assertThat(this.config.batchReceivedOnThread).startsWith("dispatching-rabbitmq-amqp-"); } + @Test + void verifyBasicRequestReply() { + CompletableFuture replyFuture = this.template.convertSendAndReceive("requestQueue", "test data"); + assertThat(replyFuture).succeedsWithin(10, TimeUnit.SECONDS).isEqualTo("TEST DATA"); + } + + @Test + void verifyFutureReturnRequestReply() { + CompletableFuture replyFuture = this.template.convertSendAndReceive("requestQueue2", "TEST DATA2"); + assertThat(replyFuture).succeedsWithin(10, TimeUnit.SECONDS).isEqualTo("test data2"); + } + + @Test + void verifyMonoReturnRequestReply() { + CompletableFuture replyFuture = this.template.convertSendAndReceive("requestQueue3", "test data3"); + assertThat(replyFuture).succeedsWithin(10, TimeUnit.SECONDS).isEqualTo("Mono test data3"); + } + + @Test + void verifyReplyOnAnotherQueue() { + this.template.convertAndSend("requestQueue4", "test data4"); + CompletableFuture replyFuture = this.template.receiveAndConvert("q4"); + assertThat(replyFuture).succeedsWithin(10, TimeUnit.SECONDS) + .isEqualTo("Reply for 'test data4' via 'e1' and 'k4'"); + } + @Configuration @EnableRabbit static class Config { @@ -163,6 +194,21 @@ Queue q3() { return new Queue("q3"); } + @Bean + DirectExchange e1() { + return new DirectExchange("e1"); + } + + @Bean + Queue q4() { + return new Queue("q4"); + } + + @Bean + Binding b4() { + return BindingBuilder.bind(q4()).to(e1()).with("k4"); + } + @Bean(RabbitListenerAnnotationBeanPostProcessor.DEFAULT_RABBIT_LISTENER_CONTAINER_FACTORY_BEAN_NAME) RabbitAmqpListenerContainerFactory rabbitAmqpListenerContainerFactory(AmqpConnectionFactory connectionFactory) { return new RabbitAmqpListenerContainerFactory(connectionFactory); @@ -231,6 +277,29 @@ void processBatchFromQ3(List data) { this.batchReceived.complete(data); } + @RabbitListener(queuesToDeclare = @org.springframework.amqp.rabbit.annotation.Queue("requestQueue")) + String toUpperCaseRpc(String data) { + return data.toUpperCase(); + } + + @RabbitListener(queuesToDeclare = @org.springframework.amqp.rabbit.annotation.Queue("requestQueue2")) + CompletableFuture toLowerCaseFutureRpc(String data) { + return CompletableFuture.completedFuture(data) + .thenApply(String::toLowerCase); + } + + @RabbitListener(queuesToDeclare = @org.springframework.amqp.rabbit.annotation.Queue("requestQueue3")) + Mono monoRpc(String data) { + return Mono.just(data) + .map(value -> "Mono " + value); + } + + @RabbitListener(queuesToDeclare = @org.springframework.amqp.rabbit.annotation.Queue("requestQueue4")) + @SendTo("e1/k4") + String replyViaSendTo(String data) { + return "Reply for '%s' via 'e1' and 'k4'".formatted(data); + } + } } From 2cb65d57bec95d352c14844d10b573ded046f9e9 Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Fri, 14 Mar 2025 00:38:39 +0700 Subject: [PATCH 717/737] GH-3015: Use backtick formatted property in docs Fixes: #3015 Signed-off-by: Tran Ngoc Nhan **Auto-cherry-pick to `3.2.x`** --- .../antora/modules/ROOT/pages/amqp/containerAttributes.adoc | 2 +- .../appendix/previous-whats-new/changes-in-1-3-since-1-2.adoc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/reference/antora/modules/ROOT/pages/amqp/containerAttributes.adoc b/src/reference/antora/modules/ROOT/pages/amqp/containerAttributes.adoc index f3172bf9fe..80440df9c7 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/containerAttributes.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/containerAttributes.adoc @@ -276,7 +276,7 @@ a| |The number of retry attempts when passive queue declaration fails. Passive queue declaration occurs when the consumer starts or, when consuming from multiple queues, when not all queues were available during initialization. -When none of the configured queues can be passively declared (for any reason) after the retries are exhausted, the container behavior is controlled by the 'missingQueuesFatal` property, described earlier. +When none of the configured queues can be passively declared (for any reason) after the retries are exhausted, the container behavior is controlled by the `missingQueuesFatal` property, described earlier. Default: Three retries (for a total of four attempts). a|image::tickmark.png[] diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-3-since-1-2.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-3-since-1-2.adoc index 41ddf37416..da18f901ca 100644 --- a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-3-since-1-2.adoc +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-1-3-since-1-2.adoc @@ -98,7 +98,7 @@ This error handler detects fatal message conversion problems and instructs the c See xref:amqp/exception-handling.adoc[Exception Handling]. [[listener-container-missingqueuesfatal-property-since-1-3-5]] -== Listener Container 'missingQueuesFatal` Property (Since 1.3.5) +== Listener Container `missingQueuesFatal` Property (Since 1.3.5) The `SimpleMessageListenerContainer` now has a property called `missingQueuesFatal` (default: `true`). Previously, missing queues were always fatal. From 684fac25f2b74f35c2e7e115b0ba3cfd4e3f480b Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 13 Mar 2025 15:48:24 -0400 Subject: [PATCH 718/737] Fix docs for version `4.0` * Move existing `What's New` to the respective `changes-in-3-2-since-3-1.adoc` * Add entries for the current version into `What's New` * Start new `rabbitmq-amqp-client.adoc` chapter --- src/reference/antora/modules/ROOT/nav.adoc | 1 + .../changes-in-3-2-since-3-1.adoc | 17 ++++++++++++ .../ROOT/pages/rabbitmq-amqp-client.adoc | 8 ++++++ .../antora/modules/ROOT/pages/whats-new.adoc | 26 +++++++++++-------- 4 files changed, 41 insertions(+), 11 deletions(-) create mode 100644 src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-3-2-since-3-1.adoc create mode 100644 src/reference/antora/modules/ROOT/pages/rabbitmq-amqp-client.adoc diff --git a/src/reference/antora/modules/ROOT/nav.adoc b/src/reference/antora/modules/ROOT/nav.adoc index b4327b384a..8bc51e5acd 100644 --- a/src/reference/antora/modules/ROOT/nav.adoc +++ b/src/reference/antora/modules/ROOT/nav.adoc @@ -57,6 +57,7 @@ *** xref:amqp/multi-rabbit.adoc[] *** xref:amqp/debugging.adoc[] ** xref:stream.adoc[] +** xref:rabbitmq-amqp-client.adoc[] ** xref:logging.adoc[] ** xref:sample-apps.adoc[] ** xref:testing.adoc[] diff --git a/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-3-2-since-3-1.adoc b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-3-2-since-3-1.adoc new file mode 100644 index 0000000000..cecbc5f8ec --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/appendix/previous-whats-new/changes-in-3-2-since-3-1.adoc @@ -0,0 +1,17 @@ +[[changes-in-3-2-since-3-1]] += Changes in 3.2 Since 3.1 + +[[spring-framework-6-2]] +== Spring Framework 6.1 + +This version requires Spring Framework 6.2. + +[[x32-consistent-hash-exchange]] +== Consistent Hash Exchange + +The convenient `ConsistentHashExchange` and respective `ExchangeBuilder.consistentHashExchange()` API has been introduced. + +[[x32-retry-count-header]] +== The `retry_count` header + +The `retry_count` header should be used now instead of relying on server side increment for the `x-death.count` property. \ No newline at end of file diff --git a/src/reference/antora/modules/ROOT/pages/rabbitmq-amqp-client.adoc b/src/reference/antora/modules/ROOT/pages/rabbitmq-amqp-client.adoc new file mode 100644 index 0000000000..a1e2fce851 --- /dev/null +++ b/src/reference/antora/modules/ROOT/pages/rabbitmq-amqp-client.adoc @@ -0,0 +1,8 @@ +[[amqp-client]] += RabbitMQ AMQP 1.0 Support + +Version 4.0 introduces `spring-rabbitmq-client` module for https://www.rabbitmq.com/client-libraries/amqp-client-libraries[AMQP 1.0] protocol support on RabbitMQ. + +This artifact is based on the {rabbitmq-github}/rabbitmq-amqp-java-client[com.rabbitmq.client:amqp-client] library and therefore can work only with RabbitMQ and its AMQP 1.0 protocol support. +It cannot be used for any arbitrary AMQP 1.0 broker. +For that purpose a https://qpid.apache.org/components/jms/index.html[JMS bridge] and respective {spring-framework-docs}/integration/jms.html[Spring JMS] integration is recommended so far. \ No newline at end of file diff --git a/src/reference/antora/modules/ROOT/pages/whats-new.adoc b/src/reference/antora/modules/ROOT/pages/whats-new.adoc index 0e643ec6ca..3031c1cb17 100644 --- a/src/reference/antora/modules/ROOT/pages/whats-new.adoc +++ b/src/reference/antora/modules/ROOT/pages/whats-new.adoc @@ -2,20 +2,24 @@ = What's New :page-section-summary-toc: 1 -[[changes-in-3-2-since-3-1]] -== Changes in 3.2 Since 3.1 +[[changes-in-4-2-since-3-2]] +== Changes in 4.0 Since 3.2 -[[spring-framework-6-2]] -=== Spring Framework 6.1 +[[spring-framework-7-0]] +=== Spring Framework 7.0 -This version requires Spring Framework 6.2. +This version requires Spring Framework 7.0. -[[x32-consistent-hash-exchange]] -=== Consistent Hash Exchange +[[x40-null-away]] +=== Null-safety -The convenient `ConsistentHashExchange` and respective `ExchangeBuilder.consistentHashExchange()` API has been introduced. +As many other Spring portfolio projects, Spring AMQP has been migrated to https://jspecify.dev/docs/start-here[JSpecify] annotations to declare the nullness of API. +The https://github.com/uber/NullAway[NullAway] Gradle plugin is used to check the consistency of null-safety declarations. -[[x32-retry-count-header]] -=== The `retry_count` header +[[x40-rabbitmq-amqp-client]] +=== The `spring-rabbitmq-client` module -The `retry_count` header should be used now instead of relying on server side increment for the `x-death.count` property. \ No newline at end of file +The new `spring-rabbitmq-client` module (with same artifact name) is introduced. +This is an implementation of AMQP 1.0 protocol specific to RabbitMQ since `4.0` and based on the `com.rabbitmq.client:amqp-client` library. + +See xref:rabbitmq-amqp-client.adoc[] for more information. \ No newline at end of file From 84ebd3da465848682f95295c809a8abd5cb2b58b Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 14 Mar 2025 16:33:18 -0400 Subject: [PATCH 719/737] GH-2919: Add documentation for `spring-rabbitmq-client` Fixes: https://github.com/spring-projects/spring-amqp/issues/2919 --- .../ROOT/pages/rabbitmq-amqp-client.adoc | 266 +++++++++++++++++- 1 file changed, 265 insertions(+), 1 deletion(-) diff --git a/src/reference/antora/modules/ROOT/pages/rabbitmq-amqp-client.adoc b/src/reference/antora/modules/ROOT/pages/rabbitmq-amqp-client.adoc index a1e2fce851..ae3cbf1555 100644 --- a/src/reference/antora/modules/ROOT/pages/rabbitmq-amqp-client.adoc +++ b/src/reference/antora/modules/ROOT/pages/rabbitmq-amqp-client.adoc @@ -5,4 +5,268 @@ Version 4.0 introduces `spring-rabbitmq-client` module for https://www.rabbitmq. This artifact is based on the {rabbitmq-github}/rabbitmq-amqp-java-client[com.rabbitmq.client:amqp-client] library and therefore can work only with RabbitMQ and its AMQP 1.0 protocol support. It cannot be used for any arbitrary AMQP 1.0 broker. -For that purpose a https://qpid.apache.org/components/jms/index.html[JMS bridge] and respective {spring-framework-docs}/integration/jms.html[Spring JMS] integration is recommended so far. \ No newline at end of file +For that purpose a https://qpid.apache.org/components/jms/index.html[JMS bridge] and respective {spring-framework-docs}/integration/jms.html[Spring JMS] integration is recommended so far. + +This dependency has to be added to the project to be able to interact with RabbitMQ AMQP 1.0 support: + +.maven +[source,xml,subs="+attributes"] +---- + + org.springframework.amqp + spring-rabbitmq-client + {project-version} + +---- + +.gradle +[source,groovy,subs="+attributes"] +---- +compile 'org.springframework.amqp:spring-rabbitmq-client:{project-version}' +---- + +The `spring-rabbit` (for AMQP 0.9.1 protocol) comes as a transitive dependency for reusing some common API in this new client, for example, exceptions, the `@RabbitListener` support. +It is not necessary to use both functionality in the target project, but RabbitMQ allows both AMQP 0.9.1 and 1.0 co-exists. + +For more information about RabbitMQ AMQP 1.0 Java Client see its https://www.rabbitmq.com/client-libraries/amqp-client-libraries[documentation]. + +[[amqp-client-environment]] +== RabbitMQ AMQP 1.0 Environment + +The `com.rabbitmq.client.amqp.Environment` is the first thing which has to be added to the project for connection management and other common settings. +It is an entry point to a node or a cluster of nodes. +The environment allows creating connections. +It can contain infrastructure-related configuration settings shared between connections, e.g. pools of threads, metrics and/or observation: + +[source,java] +---- +@Bean +Environment environment() { + return new AmqpEnvironmentBuilder() + .connectionSettings() + .port(5672) + .environmentBuilder() + .build(); +} +---- + +The same `Environment` instance can be used for connecting to different RabbitMQ brokers, then connection setting must be provided on specific connection. +See below. + +[[amqp-client-connection-factory]] +== AMQP Connection Factory + +The `org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory` abstraction was introduced to manage `com.rabbitmq.client.amqp.Connection`. +Don't confuse it with a `org.springframework.amqp.rabbit.connection.ConnectionFactory` which is only for AMQP 0.9.1 protocol. +The `SingleAmqpConnectionFactory` implementation is present to manage one connection and its settings. +The same `Connection` can be shared between many producers, consumers and management. +The multi-plexing is handled by the link abstraction for AMQP 1.0 protocol implementation internally in the AMQP client library. +The `Connection` has recovery capabilities and also handles topology. + +In most cases there is just enough to add this bean into the project: + +[source,java] +---- +@Bean +AmqpConnectionFactory connectionFactory(Environment environment) { + return new SingleAmqpConnectionFactory(environment); +} +---- + +See `SingleAmqpConnectionFactory` setters for all connection-specific setting. + +[[amqp-client-topology]] +== RabbitMQ Topology Management + +For topology management (exchanges, queues and binding between) from the application perspective, the `RabbitAmqpAdmin` is present, which is an implementation of existing `AmqpAdmin` interface: + +[source,java] +---- +@Bean +RabbitAmqpAdmin admin(AmqpConnectionFactory connectionFactory) { + return new RabbitAmqpAdmin(connectionFactory); +} +---- + +The same bean definitions for `Exchange`, `Queue`, `Binding` and `Declarables` instances as described in the xref:amqp/broker-configuration.adoc[] has to be used to manage topology. +The `RabbitAdmin` from `spring-rabbit` can also do that, but it happens against AMQP 0.9.1 connection, and since `RabbitAmqpAdmin` is based on the AMQP 1.0 connection, the topology recovery is handled smoothly from there, together with publishers and consumers recovery. + +The `RabbitAmqpAdmin` performs respective beans scanning in its `start()` lifecycle callback. +The `initialize()`, as well-as all other RabbitMQ entities management methods can be called manually at runtime. +Internally the `RabbitAmqpAdmin` uses `com.rabbitmq.client.amqp.Connection.management()` API to perform respective topology manipulations. + +[[amqp-client-template]] +== `RabbitAmqpTemplate` + +The `RabbitAmqpTemplate` is an implementation of the `AsyncAmqpTemplate` and performs various send/receive operations with AMQP 1.0 protocol. +Requires an `AmqpConnectionFactory` and can be configured with some defaults. +Even if `com.rabbitmq.client:amqp-client` library comes with a `com.rabbitmq.client.amqp.Message`, the `RabbitAmqpTemplate` still exposes an API based on the well-known `org.springframework.amqp.core.Message` with all the supporting classes like `MessageProperties` and `MessageConverter` abstraction. +The conversion to/from `com.rabbitmq.client.amqp.Message` is done internally in the `RabbitAmqpTemplate`. +All the methods return a `CompletableFuture` to obtain operation results eventually. +The operations with plain object require message body conversion and `SimpleMessageConverter` is used by default. +See xref:amqp/message-converters.adoc[] for more information about conversions. + +Usually, just one bean like this is enough to perform all the possible template pattern operation: + +[source,java] +---- +@Bean +RabbitAmqpTemplate rabbitTemplate(AmqpConnectionFactory connectionFactory) { + return new RabbitAmqpTemplate(connectionFactory); +} +---- + +It can be configured for some default exchange and routing key or just queue. +The `RabbitAmqpTemplate` have a default queue for receive operation and another default queue for request-reply operation where temporary queue is created for the request by the client if not present. + +Here are some samples of `RabbitAmqpTemplate` operations: + +[source,java] +---- +@Bean +DirectExchange e1() { + return new DirectExchange("e1"); +} + +@Bean +Queue q1() { + return QueueBuilder.durable("q1").deadLetterExchange("dlx1").build(); +} + +@Bean +Binding b1() { + return BindingBuilder.bind(q1()).to(e1()).with("k1"); +} + +... + +@Test +void defaultExchangeAndRoutingKey() { + this.rabbitAmqpTemplate.setExchange("e1"); + this.rabbitAmqpTemplate.setRoutingKey("k1"); + this.rabbitAmqpTemplate.setReceiveQueue("q1"); + + assertThat(this.rabbitAmqpTemplate.convertAndSend("test1")) + .succeedsWithin(Duration.ofSeconds(10)); + + assertThat(this.rabbitAmqpTemplate.receiveAndConvert()) + .succeedsWithin(Duration.ofSeconds(10)) + .isEqualTo("test1"); +} +---- + +Here we declared an `e1` exchange, `q1` queue and bind it into that exchange with a `k1` routing key. +Then we use a default setting for `RabbitAmqpTemplate` to publish messages to the mentioned exchange with the respective routing key and use `q1` as default queue for receiving operations. +There are overloaded variants for those methods to send to specific exchange or queue (for send and receive). +The `receiveAndConvert()` operations with a `ParameterizedTypeReference` requires a `SmartMessageConverter` to be injected into the `RabbitAmqpTemplate`. + +The next example demonstrate and RPC implementation with `RabbitAmqpTemplate` (assuming same RabbitMQ objects as in the previous example): + +[source,java] +---- +@Test +void verifyRpc() { + String testRequest = "rpc-request"; + String testReply = "rpc-reply"; + + CompletableFuture rpcClientResult = this.template.convertSendAndReceive("e1", "k1", testRequest); + + AtomicReference receivedRequest = new AtomicReference<>(); + CompletableFuture rpcServerResult = + this.rabbitAmqpTemplate.receiveAndReply("q1", + payload -> { + receivedRequest.set(payload); + return testReply; + }); + + assertThat(rpcServerResult).succeedsWithin(Duration.ofSeconds(10)).isEqualTo(true); + assertThat(rpcClientResult).succeedsWithin(Duration.ofSeconds(10)).isEqualTo(testReply); + assertThat(receivedRequest.get()).isEqualTo(testRequest); +} +---- + +The correlation and `replyTo` queue are managed internally. +The server side can be implemented with a `@RabbitListener` POJO method described below. + +[[amqp-client-listener]] +== The RabbitMQ AMQP 1.0 Consumer + +As with many other messaging implementations for consumer side, the `spring-rabbitmq-client` modules comes with the `RabbitAmqpListenerContainer` which is, essentially, an implementation of well-know `MessageListenerContainer`. +It does exactly the same as `DirectMessageListenerContainer`, but for RabbitMQ AMQP 1.0 support. +Requires an `AmqpConnectionFactory` and at least one queue to consume from. +Also, the `MessageListener` (or AMQP 1.0 specific `RabbitAmqpMessageListener`) must be provided. +Can be configured with an `autoSettle = false`, with the meaning of `AcknowledgeMode.MANUAL`. +In that case, the `Message` provided to the `MessageListener` has in its `MessageProperties` an `AmqpAcknowledgment` callback for target logic consideration. + +The `RabbitAmqpMessageListener` has a contract for `com.rabbitmq.client:amqp-client` abstractions: + +[source,java] +---- +/** + * Process an AMQP message. + * @param message the message to process. + * @param context the consumer context to settle message. + * Null if container is configured for {@code autoSettle}. + */ +void onAmqpMessage(Message message, Consumer.Context context); +---- + +Where the first argument is a native received `com.rabbitmq.client.amqp.Message` and `context` is a native callback for message settlement, similar to the mentioned above `AmqpAcknowledgment` abstraction. + +The `RabbitAmqpMessageListener` can handle and settle messages in batches when `batchSize` option is provided. +For this purpose the `MessageListener.onMessageBatch()` contract must be implemented. +The `batchReceiveDuration` option is used to schedule a force release for not full batches to avoid memory and https://www.rabbitmq.com/blog/2024/09/02/amqp-flow-control[consumer credits] exhausting. + +Usually, the `RabbitAmqpMessageListener` class is not used directly in the target project, and POJO method annotation configuration via `@RabbitListener` is chosen for declarative consumer configuration. +The `RabbitAmqpListenerContainerFactory` must be registered under the `RabbitListenerAnnotationBeanPostProcessor.DEFAULT_RABBIT_LISTENER_CONTAINER_FACTORY_BEAN_NAME`, and `@RabbitListener` annotation process will register `RabbitAmqpMessageListener` instance into the `RabbitListenerEndpointRegistry`. +The target POJO method invocation is handled by specific `RabbitAmqpMessageListenerAdapter` implementation, which extends a `MessagingMessageListenerAdapter` and reuses a lot of its functionality, including request-reply scenarios (async or not). +So, all the concepts described in the xref:amqp/receiving-messages/async-annotation-driven.adoc[] are applied with this `RabbitAmqpMessageListener` as well. + +In addition to traditional messaging `payload` and `headers`, the `@RabbitListener` POJO method contract can be with these parameters: + +* `com.rabbitmq.client.amqp.Message` - the native AMQP 1.0 message without any conversions; +* `org.springframework.amqp.core.Message` - Spring AMQP message abstraction as conversion result from the native AMQP 1.0 message; +* `org.springframework.messaging.Message` - Spring Messaging abstraction as conversion result from the Spring AMQP message; +* `Consumer.Context` - RabbitMQ AMQP client consumer settlement API; +* `org.springframework.amqp.core.AmqpAcknowledgment` - Spring AMQP acknowledgment abstraction: delegates to the `Consumer.Context`. + +The following example demonstrates a simple `@RabbitListener` for RabbitMQ AMQP 1.0 interaction with the manual settlement: + +[source,java] +---- +@Bean(RabbitListenerAnnotationBeanPostProcessor.DEFAULT_RABBIT_LISTENER_CONTAINER_FACTORY_BEAN_NAME) +RabbitAmqpListenerContainerFactory rabbitAmqpListenerContainerFactory(AmqpConnectionFactory connectionFactory) { + return new RabbitAmqpListenerContainerFactory(connectionFactory); +} + +final List received = Collections.synchronizedList(new ArrayList<>()); + +CountDownLatch consumeIsDone = new CountDownLatch(11); + +@RabbitListener(queues = {"q1", "q2"}, + ackMode = "#{T(org.springframework.amqp.core.AcknowledgeMode).MANUAL}", + concurrency = "2", + id = "testAmqpListener") +void processQ1AndQ2Data(String data, AmqpAcknowledgment acknowledgment, Consumer.Context context) { + try { + if ("discard".equals(data)) { + if (!this.received.contains(data)) { + context.discard(); + } + else { + throw new MessageConversionException("Test message is rejected"); + } + } + else if ("requeue".equals(data) && !this.received.contains(data)) { + acknowledgment.acknowledge(AmqpAcknowledgment.Status.REQUEUE); + } + else { + acknowledgment.acknowledge(); + } + this.received.add(data); + } + finally { + this.consumeIsDone.countDown(); + } +} +---- From 346d4a7ddb01605ecd7a10af6cf0f9981da206e4 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 14 Mar 2025 16:58:03 -0400 Subject: [PATCH 720/737] Upgrade to JUnit `5.12.0` --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 70a584d593..559ae8550c 100644 --- a/build.gradle +++ b/build.gradle @@ -51,7 +51,7 @@ ext { jacksonBomVersion = '2.18.3' jaywayJsonPathVersion = '2.9.0' junit4Version = '4.13.2' - junitJupiterVersion = '5.11.4' + junitJupiterVersion = '5.12.0' kotlinCoroutinesVersion = '1.10.1' log4jVersion = '2.24.3' logbackVersion = '1.5.17' From 1d5c892a1d903d8de8b15c6a5c05bd49a8d45cd7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Mar 2025 02:33:14 +0000 Subject: [PATCH 721/737] Bump org.junit:junit-bom from 5.12.0 to 5.12.1 (#3022) Bumps [org.junit:junit-bom](https://github.com/junit-team/junit5) from 5.12.0 to 5.12.1. - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.12.0...r5.12.1) --- updated-dependencies: - dependency-name: org.junit:junit-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 559ae8550c..f022b9f6b1 100644 --- a/build.gradle +++ b/build.gradle @@ -51,7 +51,7 @@ ext { jacksonBomVersion = '2.18.3' jaywayJsonPathVersion = '2.9.0' junit4Version = '4.13.2' - junitJupiterVersion = '5.12.0' + junitJupiterVersion = '5.12.1' kotlinCoroutinesVersion = '1.10.1' log4jVersion = '2.24.3' logbackVersion = '1.5.17' From 8be69137829c9bb9fd70eb551a1bdf3492d4d06e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Mar 2025 02:33:30 +0000 Subject: [PATCH 722/737] Bump io.projectreactor:reactor-bom from 2024.0.3 to 2024.0.4 (#3023) Bumps [io.projectreactor:reactor-bom](https://github.com/reactor/reactor) from 2024.0.3 to 2024.0.4. - [Release notes](https://github.com/reactor/reactor/releases) - [Commits](https://github.com/reactor/reactor/compare/2024.0.3...2024.0.4) --- updated-dependencies: - dependency-name: io.projectreactor:reactor-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f022b9f6b1..237e6b664b 100644 --- a/build.gradle +++ b/build.gradle @@ -62,7 +62,7 @@ ext { rabbitmqAmqpClientVersion = '0.4.0' rabbitmqStreamVersion = '0.22.0' rabbitmqVersion = '5.24.0' - reactorVersion = '2024.0.3' + reactorVersion = '2024.0.4' springDataVersion = '2025.1.0-SNAPSHOT' springRetryVersion = '2.0.11' springVersion = '7.0.0-SNAPSHOT' From 063ae662a637906ccf6521fa659eecd74919916a Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 17 Mar 2025 11:38:01 -0400 Subject: [PATCH 723/737] Fix `management-rest-api.adoc` for `http-client` There is no `com.rabbitmq:http-client` (Hop) dependency anymore. The `WebClient` is used internally in the project for tests. Mention `WebClient` sample in the doc instead. * Fix `broker-configuration.adoc` for the `RabbitAdmin.getQueueInfo()` API --- .../ROOT/pages/amqp/broker-configuration.adoc | 4 ++- .../ROOT/pages/amqp/management-rest-api.adoc | 29 +++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc b/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc index 39dfa15ce5..20377cbd6b 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/broker-configuration.adoc @@ -37,6 +37,8 @@ public interface AmqpAdmin { Properties getQueueProperties(String queueName); + QueueInformation getQueueInfo(String queueName); + } ---- @@ -45,7 +47,7 @@ See also xref:amqp/template.adoc#scoped-operations[Scoped Operations]. The `getQueueProperties()` method returns some limited information about the queue (message count and consumer count). The keys for the properties returned are available as constants in the `RabbitAdmin` (`QUEUE_NAME`, `QUEUE_MESSAGE_COUNT`, and `QUEUE_CONSUMER_COUNT`). -The xref:amqp/management-rest-api.adoc#management-rest-api[RabbitMQ REST API] provides much more information in the `QueueInfo` object. +The `getQueueInfo()` returns a convenient `QueueInformation` data object. The no-arg `declareQueue()` method defines a queue on the broker with a name that is automatically generated. The additional properties of this auto-generated queue are `exclusive=true`, `autoDelete=true`, and `durable=false`. diff --git a/src/reference/antora/modules/ROOT/pages/amqp/management-rest-api.adoc b/src/reference/antora/modules/ROOT/pages/amqp/management-rest-api.adoc index 628c0ff7b1..00d31891b0 100644 --- a/src/reference/antora/modules/ROOT/pages/amqp/management-rest-api.adoc +++ b/src/reference/antora/modules/ROOT/pages/amqp/management-rest-api.adoc @@ -8,7 +8,32 @@ The `com.rabbitmq.http.client.Client` is a standard, immediate, and, therefore, It is based on the {spring-framework-docs}/web.html[Spring Web] module and its `RestTemplate` implementation. On the other hand, the `com.rabbitmq.http.client.ReactorNettyClient` is a reactive, non-blocking implementation based on the https://projectreactor.io/docs/netty/release/reference/docs/index.html[Reactor Netty] project. -The hop dependency (`com.rabbitmq:http-client`) is now also `optional`. +Also, the https://www.rabbitmq.com/docs/management#http-api-endpoints[management REST API] can be used with any HTTP client. +The next example demonstrates how to get a queue information using {spring-framework-docs}/web/webflux-webclient.html[WebClient]: -See their Javadoc for more information. +[source,java] +---- + public Map queueInfo(String queueName) throws URISyntaxException { + WebClient client = createClient("admin", "admin"); + URI uri = queueUri(queueName); + return client.get() + .uri(uri) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + }) + .block(Duration.ofSeconds(10)); + } + private URI queueUri(String queue) throws URISyntaxException { + URI uri = new URI("http://localhost:15672/api/") + .resolve("/api/queues/" + UriUtils.encodePathSegment("/", StandardCharsets.UTF_8) + "/" + queue); + return uri; + } + + private WebClient createClient(String adminUser, String adminPassword) { + return WebClient.builder() + .filter(ExchangeFilterFunctions.basicAuthentication(adminUser, adminPassword)) + .build(); + } +---- From 568b9d854b080d8019dc935c8eec4c2918053bba Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 17 Mar 2025 12:07:37 -0400 Subject: [PATCH 724/737] Move deps to latest Milestones; prepare for release --- build.gradle | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 237e6b664b..bbd4456e59 100644 --- a/build.gradle +++ b/build.gradle @@ -56,16 +56,16 @@ ext { log4jVersion = '2.24.3' logbackVersion = '1.5.17' micrometerDocsVersion = '1.0.4' - micrometerVersion = '1.15.0-SNAPSHOT' - micrometerTracingVersion = '1.5.0-SNAPSHOT' + micrometerVersion = '1.15.0-M3' + micrometerTracingVersion = '1.5.0-M3' mockitoVersion = '5.15.2' rabbitmqAmqpClientVersion = '0.4.0' rabbitmqStreamVersion = '0.22.0' - rabbitmqVersion = '5.24.0' + rabbitmqVersion = '5.25.0' reactorVersion = '2024.0.4' - springDataVersion = '2025.1.0-SNAPSHOT' + springDataVersion = '2025.1.0-M2' springRetryVersion = '2.0.11' - springVersion = '7.0.0-SNAPSHOT' + springVersion = '7.0.0-M3' testcontainersVersion = '1.20.6' javaProjects = subprojects - project(':spring-amqp-bom') From abaae9ca2835d5d6e9669d467ea5445ce4995010 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 17 Mar 2025 12:11:40 -0400 Subject: [PATCH 725/737] Fix Spring Data version to `2025.1.0-M1` --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index bbd4456e59..84b9aba9fc 100644 --- a/build.gradle +++ b/build.gradle @@ -63,7 +63,7 @@ ext { rabbitmqStreamVersion = '0.22.0' rabbitmqVersion = '5.25.0' reactorVersion = '2024.0.4' - springDataVersion = '2025.1.0-M2' + springDataVersion = '2025.1.0-M1' springRetryVersion = '2.0.11' springVersion = '7.0.0-M3' testcontainersVersion = '1.20.6' From 01091f3db00a58701adeb432e2186b9d77449691 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 17 Mar 2025 16:30:34 +0000 Subject: [PATCH 726/737] [artifactory-release] Release version 4.0.0-M2 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 0e863135dd..0a6b57472e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=4.0.0-SNAPSHOT +version=4.0.0-M2 org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true From d34f4272317401b447a5cd4b61edc17ec4f22b48 Mon Sep 17 00:00:00 2001 From: Spring Builds Date: Mon, 17 Mar 2025 16:30:35 +0000 Subject: [PATCH 727/737] [artifactory-release] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 0a6b57472e..0e863135dd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=4.0.0-M2 +version=4.0.0-SNAPSHOT org.gradle.jvmargs=-Xms512m -Xmx4g -Dfile.encoding=UTF-8 org.gradle.daemon=true org.gradle.caching=true From d3de2febc63e657de5c436ddc2f2a4fdf227dd0c Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 17 Mar 2025 15:45:35 -0400 Subject: [PATCH 728/737] Move Milestone to SNAPSHOTs * Upgrade to `awaitility-4.3.0` * Upgrade to `mockito-5.16.1` * Upgrade to `reactor-2025.0.0` * Upgrade to RabbitMQ AMQP 1.0 Client `0.5.0` --- build.gradle | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index 84b9aba9fc..64d3a731bc 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ ext { assertjVersion = '3.27.3' assertkVersion = '0.28.1' - awaitilityVersion = '4.2.2' + awaitilityVersion = '4.3.0' commonsHttpClientVersion = '5.4.2' commonsPoolVersion = '2.12.1' hamcrestVersion = '3.0' @@ -56,16 +56,16 @@ ext { log4jVersion = '2.24.3' logbackVersion = '1.5.17' micrometerDocsVersion = '1.0.4' - micrometerVersion = '1.15.0-M3' - micrometerTracingVersion = '1.5.0-M3' - mockitoVersion = '5.15.2' - rabbitmqAmqpClientVersion = '0.4.0' + micrometerVersion = '1.15.0-SNAPSHOT' + micrometerTracingVersion = '1.5.0-SNAPSHOT' + mockitoVersion = '5.16.1' + rabbitmqAmqpClientVersion = '0.5.0-SNAPSHOT' rabbitmqStreamVersion = '0.22.0' rabbitmqVersion = '5.25.0' - reactorVersion = '2024.0.4' - springDataVersion = '2025.1.0-M1' + reactorVersion = '2025.0.0-SNAPSHOT' + springDataVersion = '2025.1.0-SNAPSHOT' springRetryVersion = '2.0.11' - springVersion = '7.0.0-M3' + springVersion = '7.0.0-SNAPSHOT' testcontainersVersion = '1.20.6' javaProjects = subprojects - project(':spring-amqp-bom') @@ -136,6 +136,7 @@ allprojects { maven { url 'https://repo.spring.io/milestone' } if (version.endsWith('-SNAPSHOT')) { maven { url 'https://repo.spring.io/snapshot' } + maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } } // maven { url 'https://repo.spring.io/libs-staging-local' } } @@ -317,7 +318,7 @@ configure(javaProjects) { subproject -> checkstyle { configDirectory.set(rootProject.file('src/checkstyle')) - toolVersion = '10.21.1' + toolVersion = '10.21.4' } jar { From d6ded3fca23ada4a74a3a0e72646ff345f7f708d Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 17 Mar 2025 16:22:15 -0400 Subject: [PATCH 729/737] Fix `RabbitAmqpAdmin.purgeQueue` for proper return The `Management.queuePurge()` now returns a `PurgeStatus` abstraction, where the `messageCount` property is what we expect on the `AmqpAdmin` contract. Related to: https://github.com/rabbitmq/rabbitmq-amqp-java-client/issues/151 --- .../springframework/amqp/rabbitmq/client/RabbitAmqpAdmin.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdmin.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdmin.java index 0f93211216..21c934b7e5 100644 --- a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdmin.java +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdmin.java @@ -402,9 +402,8 @@ public void purgeQueue(String queueName, boolean noWait) { @ManagedOperation(description = "Purge a queue and return the number of messages purged") public int purgeQueue(String queueName) { try (Management management = getManagement()) { - management.queuePurge(queueName); + return (int) management.queuePurge(queueName).messageCount(); } - return 0; } @Override From 589baa634d4fea88579bc9a37d679bfa047271a6 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Mon, 17 Mar 2025 17:07:35 -0400 Subject: [PATCH 730/737] Use convenient `arguments()` API the `RabbitAmqpAdmin` Now `Management.ExchangeSpecification` and `Management.QueueSpecification` expose `arguments(Map)` API Related to: https://github.com/rabbitmq/rabbitmq-amqp-java-client/issues/153 --- .../org/springframework/amqp/core/Exchange.java | 3 --- .../amqp/rabbitmq/client/RabbitAmqpAdmin.java | 14 ++++---------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/spring-amqp/src/main/java/org/springframework/amqp/core/Exchange.java b/spring-amqp/src/main/java/org/springframework/amqp/core/Exchange.java index b6ce157623..20d977b133 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/core/Exchange.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/core/Exchange.java @@ -18,8 +18,6 @@ import java.util.Map; -import org.jspecify.annotations.Nullable; - /** * Interface for all exchanges. * @@ -63,7 +61,6 @@ public interface Exchange extends Declarable { * * @return the arguments. */ - @Nullable Map getArguments(); /** diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdmin.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdmin.java index 21c934b7e5..ada9e14a9f 100644 --- a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdmin.java +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdmin.java @@ -19,7 +19,6 @@ import java.io.IOException; import java.util.Collection; import java.util.LinkedList; -import java.util.Map; import java.util.Objects; import java.util.Properties; import java.util.concurrent.atomic.AtomicReference; @@ -281,13 +280,10 @@ private void doDeclareExchange(Management management, Exchange exchange) { Management.ExchangeSpecification exchangeSpecification = management.exchange(exchange.getName()) .type(exchange.isDelayed() ? RabbitAdmin.DELAYED_MESSAGE_EXCHANGE : exchange.getType()) -// .durable(exchange.isDurable()) // .internal(exchange.isInternal()) + .arguments(exchange.getArguments()) .autoDelete(exchange.isAutoDelete()); - Map arguments = exchange.getArguments(); - if (arguments != null) { - arguments.forEach(exchangeSpecification::argument); - } + if (exchange.isDelayed()) { exchangeSpecification.argument("x-delayed-type", exchange.getType()); } @@ -326,9 +322,9 @@ public boolean deleteExchange(String exchangeName) { .autoDelete(true) .exclusive(true) .classic() - // .durable(false) .queue() .declare(); + return new Queue(queueInfo.name(), false, true, true); } catch (AmqpException ex) { @@ -349,12 +345,10 @@ public boolean deleteExchange(String exchangeName) { management.queue(queue.getName()) .autoDelete(queue.isAutoDelete()) .exclusive(queue.isExclusive()) + .arguments(queue.getArguments()) .classic() -// .durable(queue.isDurable()) .queue(); - queue.getArguments().forEach(queueSpecification::argument); - try { String actualName = queueSpecification.declare().name(); queue.setActualName(actualName); From d2bffec92291468c905b7761ab7e487501d6af63 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 19 Mar 2025 11:36:04 -0400 Subject: [PATCH 731/737] Increase timeouts for `spring-rabbitmq-client` tests --- .../rabbitmq/client/RabbitAmqpAdminTests.java | 12 +++++----- .../client/RabbitAmqpTemplateTests.java | 22 +++++++++---------- .../listener/RabbitAmqpListenerTests.java | 18 +++++++-------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdminTests.java b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdminTests.java index 22b6561b41..b8a045ab17 100644 --- a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdminTests.java +++ b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpAdminTests.java @@ -56,13 +56,13 @@ void verifyBeanDeclarations() { template.convertAndSend("e2", "k2", "test3"), template.convertAndSend("e3", "k3", "test4"), template.convertAndSend("e4", "k4", "test5")); - assertThat(publishFutures).succeedsWithin(Duration.ofSeconds(10)); + assertThat(publishFutures).succeedsWithin(Duration.ofSeconds(20)); - assertThat(template.receiveAndConvert("q1")).succeedsWithin(Duration.ofSeconds(10)).isEqualTo("test1"); - assertThat(template.receiveAndConvert("q2")).succeedsWithin(Duration.ofSeconds(10)).isEqualTo("test2"); - assertThat(template.receiveAndConvert("q2")).succeedsWithin(Duration.ofSeconds(10)).isEqualTo("test3"); - assertThat(template.receiveAndConvert("q3")).succeedsWithin(Duration.ofSeconds(10)).isEqualTo("test4"); - assertThat(template.receiveAndConvert("q4")).succeedsWithin(Duration.ofSeconds(10)).isEqualTo("test5"); + assertThat(template.receiveAndConvert("q1")).succeedsWithin(Duration.ofSeconds(20)).isEqualTo("test1"); + assertThat(template.receiveAndConvert("q2")).succeedsWithin(Duration.ofSeconds(20)).isEqualTo("test2"); + assertThat(template.receiveAndConvert("q2")).succeedsWithin(Duration.ofSeconds(20)).isEqualTo("test3"); + assertThat(template.receiveAndConvert("q3")).succeedsWithin(Duration.ofSeconds(20)).isEqualTo("test4"); + assertThat(template.receiveAndConvert("q4")).succeedsWithin(Duration.ofSeconds(20)).isEqualTo("test5"); assertThat(declarables.getDeclarablesByType(Queue.class)) .hasSize(1) diff --git a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplateTests.java b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplateTests.java index 7cb211c6fb..49f27bf60b 100644 --- a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplateTests.java +++ b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplateTests.java @@ -84,10 +84,10 @@ void defaultExchangeAndRoutingKey() { this.rabbitAmqpTemplate.setRoutingKey("k1"); assertThat(this.rabbitAmqpTemplate.convertAndSend("test1")) - .succeedsWithin(Duration.ofSeconds(10)); + .succeedsWithin(Duration.ofSeconds(20)); assertThat(this.rabbitAmqpTemplate.receiveAndConvert("q1")) - .succeedsWithin(Duration.ofSeconds(10)) + .succeedsWithin(Duration.ofSeconds(20)) .isEqualTo("test1"); } @@ -97,10 +97,10 @@ void defaultQueues() { this.rabbitAmqpTemplate.setReceiveQueue("q1"); assertThat(this.rabbitAmqpTemplate.convertAndSend("test2")) - .succeedsWithin(Duration.ofSeconds(10)); + .succeedsWithin(Duration.ofSeconds(20)); assertThat(this.rabbitAmqpTemplate.receiveAndConvert()) - .succeedsWithin(Duration.ofSeconds(10)) + .succeedsWithin(Duration.ofSeconds(20)) .isEqualTo("test2"); } @@ -119,8 +119,8 @@ void verifyRpc() { return testReply; }); - assertThat(rpcServerResult).succeedsWithin(Duration.ofSeconds(10)).isEqualTo(true); - assertThat(rpcClientResult).succeedsWithin(Duration.ofSeconds(10)).isEqualTo(testReply); + assertThat(rpcServerResult).succeedsWithin(Duration.ofSeconds(20)).isEqualTo(true); + assertThat(rpcClientResult).succeedsWithin(Duration.ofSeconds(20)).isEqualTo(testReply); assertThat(receivedRequest.get()).isEqualTo(testRequest); this.template.send("q1", @@ -131,7 +131,7 @@ void verifyRpc() { rpcServerResult = this.rabbitAmqpTemplate.receiveAndReply("q1", payload -> "reply-attempt"); - assertThat(rpcServerResult).failsWithin(Duration.ofSeconds(10)) + assertThat(rpcServerResult).failsWithin(Duration.ofSeconds(20)) .withThrowableOfType(ExecutionException.class) .withCauseInstanceOf(AmqpIllegalStateException.class) .withRootCauseInstanceOf(IllegalArgumentException.class) @@ -141,7 +141,7 @@ void verifyRpc() { rpcClientResult = this.template.convertSendAndReceive("q1", testRequest); rpcServerResult = this.rabbitAmqpTemplate.receiveAndReply("q1", payload -> null); - assertThat(rpcServerResult).succeedsWithin(Duration.ofSeconds(10)).isEqualTo(false); + assertThat(rpcServerResult).succeedsWithin(Duration.ofSeconds(20)).isEqualTo(false); assertThat(rpcClientResult).failsWithin(Duration.ofSeconds(2)) .withThrowableThat() .isInstanceOf(TimeoutException.class); @@ -149,17 +149,17 @@ void verifyRpc() { this.template.convertSendAndReceive("q1", new byte[0]); rpcServerResult = this.rabbitAmqpTemplate.receiveAndReply("q1", payload -> payload); - assertThat(rpcServerResult).failsWithin(Duration.ofSeconds(10)) + assertThat(rpcServerResult).failsWithin(Duration.ofSeconds(20)) .withThrowableOfType(ExecutionException.class) .withCauseInstanceOf(AmqpIllegalStateException.class) .withRootCauseInstanceOf(ClassCastException.class) .withMessageContaining("Failed to process RPC request: (Body:'[B") .withStackTraceContaining("class [B cannot be cast to class java.lang.String"); - assertThat(this.template.receiveAndConvert("dlq1")).succeedsWithin(10, TimeUnit.SECONDS) + assertThat(this.template.receiveAndConvert("dlq1")).succeedsWithin(20, TimeUnit.SECONDS) .isEqualTo("non-rpc-request"); - assertThat(this.template.receiveAndConvert("dlq1")).succeedsWithin(10, TimeUnit.SECONDS) + assertThat(this.template.receiveAndConvert("dlq1")).succeedsWithin(20, TimeUnit.SECONDS) .isEqualTo(new byte[0]); } diff --git a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java index 17d68e1752..426f8f9097 100644 --- a/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java +++ b/spring-rabbitmq-client/src/test/java/org/springframework/amqp/rabbitmq/client/listener/RabbitAmqpListenerTests.java @@ -92,14 +92,14 @@ void verifyAllDataIsConsumedFromQ1AndQ2() throws InterruptedException { this.template.convertAndSend((random.nextInt(2) == 0 ? "q1" : "q2"), testData); } - assertThat(this.config.consumeIsDone.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(this.config.consumeIsDone.await(20, TimeUnit.SECONDS)).isTrue(); synchronized (this.config.received) { assertThat(this.config.received).containsAll(testDataList); } - assertThat(this.template.receive("dlq1")).succeedsWithin(10, TimeUnit.SECONDS); - assertThat(this.template.receive("dlq1")).succeedsWithin(10, TimeUnit.SECONDS); + assertThat(this.template.receive("dlq1")).succeedsWithin(20, TimeUnit.SECONDS); + assertThat(this.template.receive("dlq1")).succeedsWithin(20, TimeUnit.SECONDS); } @Test @@ -112,7 +112,7 @@ void verifyBatchConsumedAfterScheduledTimeout() { this.template.convertAndSend("q3", testData); } - assertThat(this.config.batchReceived).succeedsWithin(10, TimeUnit.SECONDS) + assertThat(this.config.batchReceived).succeedsWithin(20, TimeUnit.SECONDS) .asInstanceOf(InstanceOfAssertFactories.LIST) .hasSize(5) .containsAll(testDataList); @@ -141,7 +141,7 @@ void verifyBatchConsumedAfterScheduledTimeout() { this.template.convertAndSend("q3", testData); } - assertThat(this.config.batchReceived).succeedsWithin(10, TimeUnit.SECONDS) + assertThat(this.config.batchReceived).succeedsWithin(20, TimeUnit.SECONDS) .asInstanceOf(InstanceOfAssertFactories.LIST) .hasSize(10) .containsAll(testDataList); @@ -152,26 +152,26 @@ void verifyBatchConsumedAfterScheduledTimeout() { @Test void verifyBasicRequestReply() { CompletableFuture replyFuture = this.template.convertSendAndReceive("requestQueue", "test data"); - assertThat(replyFuture).succeedsWithin(10, TimeUnit.SECONDS).isEqualTo("TEST DATA"); + assertThat(replyFuture).succeedsWithin(20, TimeUnit.SECONDS).isEqualTo("TEST DATA"); } @Test void verifyFutureReturnRequestReply() { CompletableFuture replyFuture = this.template.convertSendAndReceive("requestQueue2", "TEST DATA2"); - assertThat(replyFuture).succeedsWithin(10, TimeUnit.SECONDS).isEqualTo("test data2"); + assertThat(replyFuture).succeedsWithin(20, TimeUnit.SECONDS).isEqualTo("test data2"); } @Test void verifyMonoReturnRequestReply() { CompletableFuture replyFuture = this.template.convertSendAndReceive("requestQueue3", "test data3"); - assertThat(replyFuture).succeedsWithin(10, TimeUnit.SECONDS).isEqualTo("Mono test data3"); + assertThat(replyFuture).succeedsWithin(20, TimeUnit.SECONDS).isEqualTo("Mono test data3"); } @Test void verifyReplyOnAnotherQueue() { this.template.convertAndSend("requestQueue4", "test data4"); CompletableFuture replyFuture = this.template.receiveAndConvert("q4"); - assertThat(replyFuture).succeedsWithin(10, TimeUnit.SECONDS) + assertThat(replyFuture).succeedsWithin(20, TimeUnit.SECONDS) .isEqualTo("Reply for 'test data4' via 'e1' and 'k4'"); } From 8aa38bd0bbdcdc61f580e1f4487b9c5f0ec93223 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Wed, 19 Mar 2025 12:13:08 -0400 Subject: [PATCH 732/737] Use `AtomicBoolean` for receive ot ignore other messages The `RabbitAmqpTemplate.receive()` uses an AMQP 1.0 `Consumer` to receive a single message. There is a race condition when we have several messages in the queue, so after receiving one, this consumer may deliver the next one. * Use `AtomicBoolean messageReceived` to accept only one message from the consumer. Since the `CompletableFuture` closes that consumer on its completion, there is just small chance that a new message would be handed to the handler. And since we do nothing with this new message, it will come back to the queue after we close consumer. --- .../amqp/rabbitmq/client/RabbitAmqpTemplate.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplate.java b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplate.java index 559f744b43..27b2d0cf21 100644 --- a/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplate.java +++ b/spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplate.java @@ -20,6 +20,7 @@ import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Supplier; @@ -321,6 +322,8 @@ public CompletableFuture receive() { public CompletableFuture receive(String queueName) { CompletableFuture messageFuture = new CompletableFuture<>(); + AtomicBoolean messageReceived = new AtomicBoolean(); + Consumer consumer = this.connectionFactory.getConnection() .consumerBuilder() @@ -328,8 +331,10 @@ public CompletableFuture receive(String queueName) { .initialCredits(1) .priority(10) .messageHandler((context, message) -> { - context.accept(); - messageFuture.complete(RabbitAmqpUtils.fromAmqpMessage(message, null)); + if (messageReceived.compareAndSet(false, true)) { + context.accept(); + messageFuture.complete(RabbitAmqpUtils.fromAmqpMessage(message, null)); + } }) .build(); From 21595f16c352d7995eb4c0606a53f211c7a554d7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 23 Mar 2025 07:39:42 -0400 Subject: [PATCH 733/737] Bump ch.qos.logback:logback-classic from 1.5.17 to 1.5.18 (#3027) Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.17 to 1.5.18. - [Release notes](https://github.com/qos-ch/logback/releases) - [Commits](https://github.com/qos-ch/logback/compare/v_1.5.17...v_1.5.18) --- updated-dependencies: - dependency-name: ch.qos.logback:logback-classic dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 64d3a731bc..affdace20e 100644 --- a/build.gradle +++ b/build.gradle @@ -54,7 +54,7 @@ ext { junitJupiterVersion = '5.12.1' kotlinCoroutinesVersion = '1.10.1' log4jVersion = '2.24.3' - logbackVersion = '1.5.17' + logbackVersion = '1.5.18' micrometerDocsVersion = '1.0.4' micrometerVersion = '1.15.0-SNAPSHOT' micrometerTracingVersion = '1.5.0-SNAPSHOT' From becce4e376ae56eed69ca595d37ae9db4ca49b06 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 23 Mar 2025 07:40:29 -0400 Subject: [PATCH 734/737] Bump kotlinVersion from 2.1.10 to 2.1.20 (#3028) Bumps `kotlinVersion` from 2.1.10 to 2.1.20. Updates `org.jetbrains.kotlin:kotlin-gradle-plugin` from 2.1.10 to 2.1.20 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v2.1.10...v2.1.20) Updates `org.jetbrains.kotlin:kotlin-allopen` from 2.1.10 to 2.1.20 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v2.1.10...v2.1.20) --- updated-dependencies: - dependency-name: org.jetbrains.kotlin:kotlin-gradle-plugin dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.jetbrains.kotlin:kotlin-allopen dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index affdace20e..1d85bae2b2 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlinVersion = '2.1.10' + ext.kotlinVersion = '2.1.20' ext.isCI = System.getenv('GITHUB_ACTION') repositories { mavenCentral() From 993e94a5fc2dde3929460b4b1576f5f893445f11 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 28 Mar 2025 11:27:11 -0400 Subject: [PATCH 735/737] GH-3032: Fix BlockingQueueConsumer for in-flight draining Fixes: #3032 Issue link: https://github.com/spring-projects/spring-amqp/issues/3032 The fix for https://github.com/spring-projects/spring-amqp/issues/2941 has missed "in-flight draining" for non-transactional consumers. **Auto-cherry-pick to `3.2.x` & `3.1.x`** --- .../listener/BlockingQueueConsumer.java | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java index 3f3d7e53a7..0441d126b4 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/BlockingQueueConsumer.java @@ -540,10 +540,7 @@ private void checkShutdown() { */ @Nullable public Message nextMessage() throws InterruptedException, ShutdownSignalException { - if (logger.isTraceEnabled()) { - logger.trace("Retrieving delivery for " + this); - } - return handle(this.queue.take()); + return nextMessage(-1); } /** @@ -562,26 +559,35 @@ public Message nextMessage(long timeout) throws InterruptedException, ShutdownSi if (!this.missingQueues.isEmpty()) { checkMissingQueues(); } - if (!cancelled()) { - Message message = handle(this.queue.poll(timeout, TimeUnit.MILLISECONDS)); - if (message != null && cancelled()) { - this.activeObjectCounter.release(this); - ConsumerCancelledException consumerCancelledException = new ConsumerCancelledException(); - rollbackOnExceptionIfNecessary(consumerCancelledException, - message.getMessageProperties().getDeliveryTag()); - throw consumerCancelledException; + + if (this.transactional && cancelled()) { + throw consumerCancelledException(null); + } + else { + Message message = handle(timeout < 0 ? this.queue.take() : this.queue.poll(timeout, TimeUnit.MILLISECONDS)); + if (cancelled() && (message == null || this.transactional)) { + Long deliveryTagToNack = null; + if (message != null) { + deliveryTagToNack = message.getMessageProperties().getDeliveryTag(); + } + throw consumerCancelledException(deliveryTagToNack); } else { return message; } } + } + + private ConsumerCancelledException consumerCancelledException(@Nullable Long deliveryTagToNack) { + this.activeObjectCounter.release(this); + ConsumerCancelledException consumerCancelledException = new ConsumerCancelledException(); + if (deliveryTagToNack != null) { + rollbackOnExceptionIfNecessary(consumerCancelledException, deliveryTagToNack); + } else { this.deliveryTags.clear(); - this.activeObjectCounter.release(this); - ConsumerCancelledException consumerCancelledException = new ConsumerCancelledException(); - rollbackOnExceptionIfNecessary(consumerCancelledException); - throw consumerCancelledException; } + return consumerCancelledException; } /* From 7d089af553f9b4050fbe39acf7327f57014074a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 29 Mar 2025 08:10:37 -0400 Subject: [PATCH 736/737] Bump the development-dependencies group with 2 updates Bumps the development-dependencies group with 2 updates: [com.uber.nullaway:nullaway](https://github.com/uber/NullAway) and [org.apache.httpcomponents.client5:httpclient5](https://github.com/apache/httpcomponents-client). Updates `com.uber.nullaway:nullaway` from 0.12.4 to 0.12.6 - [Release notes](https://github.com/uber/NullAway/releases) - [Changelog](https://github.com/uber/NullAway/blob/master/CHANGELOG.md) - [Commits](https://github.com/uber/NullAway/compare/v0.12.4...v0.12.6) Updates `org.apache.httpcomponents.client5:httpclient5` from 5.4.2 to 5.4.3 - [Changelog](https://github.com/apache/httpcomponents-client/blob/rel/v5.4.3/RELEASE_NOTES.txt) - [Commits](https://github.com/apache/httpcomponents-client/compare/rel/v5.4.2...rel/v5.4.3) --- updated-dependencies: - dependency-name: com.uber.nullaway:nullaway dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies - dependency-name: org.apache.httpcomponents.client5:httpclient5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 1d85bae2b2..fd72d566ba 100644 --- a/build.gradle +++ b/build.gradle @@ -44,7 +44,7 @@ ext { assertjVersion = '3.27.3' assertkVersion = '0.28.1' awaitilityVersion = '4.3.0' - commonsHttpClientVersion = '5.4.2' + commonsHttpClientVersion = '5.4.3' commonsPoolVersion = '2.12.1' hamcrestVersion = '3.0' hibernateValidationVersion = '8.0.2.Final' @@ -225,7 +225,7 @@ configure(javaProjects) { subproject -> testImplementation 'org.jetbrains.kotlin:kotlin-reflect' testImplementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' - errorprone 'com.uber.nullaway:nullaway:0.12.4' + errorprone 'com.uber.nullaway:nullaway:0.12.6' errorprone 'com.google.errorprone:error_prone_core:2.36.0' } From 1dd3f4d07804fa81a020541c1bc8238efb6fc628 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 29 Mar 2025 08:11:53 -0400 Subject: [PATCH 737/737] Bump com.rabbitmq.client:amqp-client from 0.5.0-SNAPSHOT to 0.5.0 Bumps [com.rabbitmq.client:amqp-client](https://github.com/rabbitmq/rabbitmq-amqp-java-client) from 0.5.0-SNAPSHOT to 0.5.0. - [Release notes](https://github.com/rabbitmq/rabbitmq-amqp-java-client/releases) - [Commits](https://github.com/rabbitmq/rabbitmq-amqp-java-client/commits/v0.5.0) --- updated-dependencies: - dependency-name: com.rabbitmq.client:amqp-client dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index fd72d566ba..a94ef9ef1b 100644 --- a/build.gradle +++ b/build.gradle @@ -59,7 +59,7 @@ ext { micrometerVersion = '1.15.0-SNAPSHOT' micrometerTracingVersion = '1.5.0-SNAPSHOT' mockitoVersion = '5.16.1' - rabbitmqAmqpClientVersion = '0.5.0-SNAPSHOT' + rabbitmqAmqpClientVersion = '0.5.0' rabbitmqStreamVersion = '0.22.0' rabbitmqVersion = '5.25.0' reactorVersion = '2025.0.0-SNAPSHOT'