From c388dc92f1c961b45fc4ef4fe1314e87b5579796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 18 Mar 2026 07:48:05 +0100 Subject: [PATCH] Add AMQP 1.0 client auto-configuration This commit adds auto-configuration for the generic AMQP 1.0 client in Spring AMQP 4.1, using Qpid ProtonJ. The auto-configuration provides an AmqpConnectionFactory as well as an AmqpClient with standard customizer callbacks. The "spring.amqp" namespace exposes settings to connect to an AMQP 1.0 compliant broker. Docker compose and testcontainers support using RabbitMQ have been added too. Closes gh-49621 --- .../DocumentConfigurationProperties.java | 1 + documentation/spring-boot-docs/build.gradle | 1 + .../antora/modules/ROOT/pages/redirect.adoc | 14 +- .../pages/features/dev-services.adoc | 3 + .../reference/pages/messaging/amqp.adoc | 64 +++- .../pages/testing/testcontainers.adoc | 3 + .../amqp/generic/sending/MyBean.java | 37 +++ .../amqp/{ => rabbitmq}/receiving/MyBean.java | 2 +- .../receiving/custom/MyBean.java | 2 +- .../receiving/custom/MyMessageConverter.java | 2 +- .../custom/MyRabbitConfiguration.java | 2 +- .../amqp/{ => rabbitmq}/sending/MyBean.java | 2 +- .../messaging/amqp/generic/sending/MyBean.kt | 32 ++ .../amqp/{ => rabbitmq}/receiving/MyBean.kt | 2 +- .../{ => rabbitmq}/receiving/custom/MyBean.kt | 2 +- .../receiving/custom/MyMessageConverter.kt | 2 +- .../receiving/custom/MyRabbitConfiguration.kt | 2 +- .../amqp/{ => rabbitmq}/sending/MyBean.kt | 2 +- module/spring-boot-amqp/build.gradle | 61 ++++ ...AmqpAutoConfigurationIntegrationTests.java | 74 +++++ ...nectionDetailsFactoryIntegrationTests.java | 42 +++ ...nectionDetailsFactoryIntegrationTests.java | 71 +++++ .../src/dockerTest/resources/logback-test.xml | 4 + .../amqp/docker/compose/rabbitmq-compose.yaml | 8 + .../dockerTest/resources/spring.properties | 1 + .../autoconfigure/AmqpAutoConfiguration.java | 132 +++++++++ .../autoconfigure/AmqpClientCustomizer.java | 37 +++ .../autoconfigure/AmqpConnectionDetails.java | 62 ++++ .../amqp/autoconfigure/AmqpProperties.java | 125 ++++++++ .../ConnectionOptionsCustomizer.java | 39 +++ .../PropertiesAmqpConnectionDetails.java | 49 ++++ .../boot/amqp/autoconfigure/package-info.java | 23 ++ ...DockerComposeConnectionDetailsFactory.java | 84 ++++++ .../docker/compose/RabbitMqEnvironment.java | 50 ++++ .../amqp/docker/compose/package-info.java | 23 ++ ...itMqContainerConnectionDetailsFactory.java | 71 +++++ .../amqp/testcontainers/package-info.java | 23 ++ .../main/resources/META-INF/spring.factories | 5 + ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../AmqpAutoConfigurationTests.java | 276 ++++++++++++++++++ .../compose/RabbitMqEnvironmentTests.java | 72 +++++ .../spring-boot-dependencies/build.gradle | 13 + settings.gradle | 3 + .../build.gradle | 26 ++ starter/spring-boot-starter-amqp/build.gradle | 27 ++ .../RabbitMqManagementContainer.java | 57 ++++ .../boot/testsupport/container/TestImage.java | 5 +- 47 files changed, 1611 insertions(+), 28 deletions(-) create mode 100644 documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.java rename documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/{ => rabbitmq}/receiving/MyBean.java (92%) rename documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/{ => rabbitmq}/receiving/custom/MyBean.java (91%) rename documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/{ => rabbitmq}/receiving/custom/MyMessageConverter.java (93%) rename documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/{ => rabbitmq}/receiving/custom/MyRabbitConfiguration.java (95%) rename documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/{ => rabbitmq}/sending/MyBean.java (94%) create mode 100644 documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.kt rename documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/{ => rabbitmq}/receiving/MyBean.kt (92%) rename documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/{ => rabbitmq}/receiving/custom/MyBean.kt (92%) rename documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/{ => rabbitmq}/receiving/custom/MyMessageConverter.kt (92%) rename documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/{ => rabbitmq}/receiving/custom/MyRabbitConfiguration.kt (95%) rename documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/{ => rabbitmq}/sending/MyBean.kt (93%) create mode 100644 module/spring-boot-amqp/build.gradle create mode 100644 module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationIntegrationTests.java create mode 100644 module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java create mode 100644 module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitMqContainerConnectionDetailsFactoryIntegrationTests.java create mode 100644 module/spring-boot-amqp/src/dockerTest/resources/logback-test.xml create mode 100644 module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-compose.yaml create mode 100644 module/spring-boot-amqp/src/dockerTest/resources/spring.properties create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfiguration.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpClientCustomizer.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpConnectionDetails.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpProperties.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionOptionsCustomizer.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesAmqpConnectionDetails.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitMqDockerComposeConnectionDetailsFactory.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitMqEnvironment.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitMqContainerConnectionDetailsFactory.java create mode 100644 module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java create mode 100644 module/spring-boot-amqp/src/main/resources/META-INF/spring.factories create mode 100644 module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationTests.java create mode 100644 module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitMqEnvironmentTests.java create mode 100644 starter/spring-boot-starter-amqp-test/build.gradle create mode 100644 starter/spring-boot-starter-amqp/build.gradle create mode 100644 test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/RabbitMqManagementContainer.java diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java index 2191c1c1a34..dc56a5b977c 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java @@ -166,6 +166,7 @@ public abstract class DocumentConfigurationProperties extends DefaultTask { private void integrationPrefixes(Config prefix) { prefix.accept("spring.activemq"); + prefix.accept("spring.amqp"); prefix.accept("spring.artemis"); prefix.accept("spring.batch"); prefix.accept("spring.integration"); diff --git a/documentation/spring-boot-docs/build.gradle b/documentation/spring-boot-docs/build.gradle index 7f20bc5ed26..e89fb1017b0 100644 --- a/documentation/spring-boot-docs/build.gradle +++ b/documentation/spring-boot-docs/build.gradle @@ -94,6 +94,7 @@ dependencies { implementation(project(path: ":loader:spring-boot-loader-tools")) implementation(project(path: ":module:spring-boot-actuator")) implementation(project(path: ":module:spring-boot-actuator-autoconfigure")) + implementation(project(path: ":module:spring-boot-amqp")) implementation(project(path: ":module:spring-boot-cache")) implementation(project(path: ":module:spring-boot-cache-test")) implementation(project(path: ":module:spring-boot-data-cassandra")) diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc index 72414df0faa..42d3b57c37f 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/ROOT/pages/redirect.adoc @@ -1716,13 +1716,13 @@ * xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq[#features.messaging.amqp.rabbit] * xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq[#messaging.amqp.rabbit] * xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq[#messaging.amqp.rabbitmq] -* xref:reference:messaging/amqp.adoc#messaging.amqp.receiving[#boot-features-using-amqp-receiving] -* xref:reference:messaging/amqp.adoc#messaging.amqp.receiving[#features.messaging.amqp.receiving] -* xref:reference:messaging/amqp.adoc#messaging.amqp.receiving[#messaging.amqp.receiving] -* xref:reference:messaging/amqp.adoc#messaging.amqp.sending-stream[#messaging.amqp.sending-stream] -* xref:reference:messaging/amqp.adoc#messaging.amqp.sending[#boot-features-using-amqp-sending] -* xref:reference:messaging/amqp.adoc#messaging.amqp.sending[#features.messaging.amqp.sending] -* xref:reference:messaging/amqp.adoc#messaging.amqp.sending[#messaging.amqp.sending] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.receiving[#boot-features-using-amqp-receiving] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.receiving[#features.messaging.amqp.receiving] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.receiving[#messaging.amqp.receiving] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.sending-stream[#messaging.amqp.sending-stream] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.sending[#boot-features-using-amqp-sending] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.sending[#features.messaging.amqp.sending] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq.sending[#messaging.amqp.sending] * xref:reference:messaging/amqp.adoc#messaging.amqp[#boot-features-amqp] * xref:reference:messaging/amqp.adoc#messaging.amqp[#features.messaging.amqp] * xref:reference:messaging/amqp.adoc#messaging.amqp[#messaging.amqp] diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc index 36e57ff478d..f816ca64d25 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc @@ -86,6 +86,9 @@ The following service connections are currently supported: | javadoc:org.springframework.boot.activemq.autoconfigure.ActiveMQConnectionDetails[] | Containers named "symptoma/activemq" or "apache/activemq-classic" +| javadoc:org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails[] +| Containers named "rabbitmq" + | javadoc:org.springframework.boot.artemis.autoconfigure.ArtemisConnectionDetails[] | Containers named "apache/activemq-artemis" diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc index 3864f7ebe9e..15cf865bd86 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc @@ -3,7 +3,53 @@ The Advanced Message Queuing Protocol (AMQP) is a platform-neutral, wire-level protocol for message-oriented middleware. The Spring AMQP project applies core Spring concepts to the development of AMQP-based messaging solutions. -Spring Boot offers several conveniences for working with AMQP through RabbitMQ, including the `spring-boot-starter-rabbitmq` starter. +Spring Boot offers several conveniences for working with AMQP: generic AMQP 1.0 support is provided by the `spring-boot-starter-amqp` starter while specific RabbitMQ support is available via the `spring-boot-starter-rabbitmq` starter. + + + +[[messaging.amqp.generic]] +== Generic AMQP 1.0 Support + +AMQP 1.0 is supported by several brokers and messaging services beyond RabbitMQ, including ActiveMQ, Azure Service Bus, and others. +Spring AMQP provides {url-spring-amqp-docs}/amqp10-client.html[generic support for AMQP 1.0] via `org.springframework.amqp:spring-amqp-client` that is based on the https://github.com/apache/qpid-protonj2/blob/main/protonj2-client/README.md[Qpid ProtonJ2 Client Library]. + +AMQP configuration is controlled by external configuration properties in `+spring.amqp.*+`. +For example, you might declare the following in your configuration: + +[configprops,yaml] +---- +spring: + amqp: + host: "localhost" + port: 5672 + username: "admin" + password: "secret" +---- + +To configure javadoc:org.apache.qpid.protonj2.client.ConnectionOptions[connection options] of the auto-configured javadoc:org.springframework.amqp.client.AmqpConnectionFactory[], define a javadoc:org.springframework.boot.amqp.autoconfigure.ConnectionOptionsCustomizer[] bean. + +[[messaging.amqp.generic.sending]] +=== Sending a Message + +Spring's javadoc:org.springframework.amqp.client.AmqpClient[] is auto-configured, and you can autowire it directly into your own beans, as shown in the following example: + +include-code::MyBean[] + +If a javadoc:org.springframework.amqp.support.converter.MessageConverter[] bean is defined, it is associated automatically with the auto-configured javadoc:org.springframework.amqp.client.AmqpClient[]. +If no such converter is defined and Jackson is available, javadoc:org.springframework.amqp.support.converter.JacksonJsonMessageConverter[] is used. + +Client-specific settings can be configured as follows: + +[configprops,yaml] +---- +spring: + amqp: + client: + default-to-address: "/queues/default_queue" + completion-timeout: "500ms" +---- + +To further configure the auto-configured javadoc:org.springframework.amqp.client.AmqpClient[], define a javadoc:org.springframework.boot.amqp.autoconfigure.AmqpClientCustomizer[] bean. @@ -49,8 +95,8 @@ TIP: See https://spring.io/blog/2010/06/14/understanding-amqp-the-protocol-used- -[[messaging.amqp.sending]] -== Sending a Message +[[messaging.amqp.rabbitmq.sending]] +=== Sending a Message Spring's javadoc:org.springframework.amqp.core.AmqpTemplate[] and javadoc:org.springframework.amqp.core.AmqpAdmin[] are auto-configured, and you can autowire them directly into your own beans, as shown in the following example: @@ -82,8 +128,8 @@ If there's a bean of type javadoc:org.springframework.amqp.rabbit.support.microm -[[messaging.amqp.sending-stream]] -== Sending a Message To A Stream +[[messaging.amqp.rabbitmq.sending-stream]] +=== Sending a Message To A Stream To send a message to a particular stream, specify the name of the stream, as shown in the following example: @@ -101,14 +147,14 @@ If you need to create more javadoc:org.springframework.rabbit.stream.producer.Ra -[[messaging.amqp.sending-stream.ssl]] -=== SSL +[[messaging.amqp.rabbitmq.sending-stream.ssl]] +==== SSL To use SSL with RabbitMQ Streams, set configprop:spring.rabbitmq.stream.ssl.enabled[] to `true` or set configprop:spring.rabbitmq.stream.ssl.bundle[] to configure the xref:features/ssl.adoc#features.ssl.bundles[SSL bundle] to use. -[[messaging.amqp.receiving]] -== Receiving a Message +[[messaging.amqp.rabbitmq.receiving]] +=== Receiving a Message When the Rabbit infrastructure is present, any bean can be annotated with javadoc:org.springframework.amqp.rabbit.annotation.RabbitListener[format=annotation] to create a listener endpoint. If no javadoc:org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory[] has been defined, a default javadoc:org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory[] is automatically configured and you can switch to a direct container using the configprop:spring.rabbitmq.listener.type[] property. diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc index 72240022f6a..c925920a077 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc @@ -122,6 +122,9 @@ The following service connection factories are provided in the `spring-boot-test | javadoc:org.springframework.boot.activemq.autoconfigure.ActiveMQConnectionDetails[] | Containers named "symptoma/activemq" or javadoc:org.testcontainers.activemq.ActiveMQContainer[] +| javadoc:org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails[] +| Containers of type javadoc:{url-testcontainers-rabbitmq-javadoc}/org.testcontainers.rabbitmq.RabbitMQContainer[] + | javadoc:org.springframework.boot.artemis.autoconfigure.ArtemisConnectionDetails[] | Containers of type javadoc:org.testcontainers.activemq.ArtemisContainer[] diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.java new file mode 100644 index 00000000000..8f7ffc09ff5 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-present the original author 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.boot.docs.messaging.amqp.generic.sending; + +import org.springframework.amqp.client.AmqpClient; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + private final AmqpClient amqpClient; + + public MyBean(AmqpClient amqpClient) { + this.amqpClient = amqpClient; + } + + // @fold:on // ... + public void sendMessage(String msg) { + this.amqpClient.to("/queues/test").body(msg).send(); + } + // @fold:off + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.java similarity index 92% rename from documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/MyBean.java rename to documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.java index d5fd3014a05..56064bfb94c 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/MyBean.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.receiving; +package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.java similarity index 91% rename from documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyBean.java rename to documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.java index 0f0c59f87c9..671de83f0e3 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyBean.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.receiving.custom; +package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving.custom; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyMessageConverter.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.java similarity index 93% rename from documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyMessageConverter.java rename to documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.java index ba8d391a53f..18169c7e7ab 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyMessageConverter.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.receiving.custom; +package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving.custom; import org.springframework.amqp.core.Message; import org.springframework.amqp.core.MessageProperties; diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.java similarity index 95% rename from documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.java rename to documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.java index 172c9cbba0d..318428b08be 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.receiving.custom; +package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving.custom; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.connection.ConnectionFactory; diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/sending/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.java similarity index 94% rename from documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/sending/MyBean.java rename to documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.java index f2adb052cec..51b91d35769 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/sending/MyBean.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.sending; +package org.springframework.boot.docs.messaging.amqp.rabbitmq.sending; import org.springframework.amqp.core.AmqpAdmin; import org.springframework.amqp.core.AmqpTemplate; diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.kt new file mode 100644 index 00000000000..64c5174f417 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/generic/sending/MyBean.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2012-present the original author 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.boot.docs.messaging.amqp.generic.sending + +import org.springframework.amqp.client.AmqpClient +import org.springframework.stereotype.Component + +@Component +class MyBean(private val amqpClient: AmqpClient) { + + // @fold:on // ... + fun someOtherMethod(msg: String) { + amqpClient.to("/queues/test").body(msg).send() + } + // @fold:off + +} + diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.kt similarity index 92% rename from documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/MyBean.kt rename to documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.kt index a3241ddb95b..9bac053496d 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/MyBean.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.receiving +package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving import org.springframework.amqp.rabbit.annotation.RabbitListener import org.springframework.stereotype.Component diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.kt similarity index 92% rename from documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyBean.kt rename to documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.kt index f92ffc01249..3b90bf3dc42 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyBean.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.receiving.custom +package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving.custom import org.springframework.amqp.rabbit.annotation.RabbitListener import org.springframework.stereotype.Component diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyMessageConverter.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.kt similarity index 92% rename from documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyMessageConverter.kt rename to documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.kt index 7b6265875cc..03b165448e0 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyMessageConverter.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.receiving.custom +package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving.custom import org.springframework.amqp.core.Message import org.springframework.amqp.core.MessageProperties diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.kt similarity index 95% rename from documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.kt rename to documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.kt index 95ca6f61003..900866bf246 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/receiving/custom/MyRabbitConfiguration.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.receiving.custom +package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving.custom import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory import org.springframework.amqp.rabbit.connection.CachingConnectionFactory diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/sending/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.kt similarity index 93% rename from documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/sending/MyBean.kt rename to documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.kt index e8ec5274920..6d21de9bce0 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/sending/MyBean.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.sending +package org.springframework.boot.docs.messaging.amqp.rabbitmq.sending import org.springframework.amqp.core.AmqpAdmin import org.springframework.amqp.core.AmqpTemplate diff --git a/module/spring-boot-amqp/build.gradle b/module/spring-boot-amqp/build.gradle new file mode 100644 index 00000000000..7aebee0bdcb --- /dev/null +++ b/module/spring-boot-amqp/build.gradle @@ -0,0 +1,61 @@ +/* + * Copyright 2012-present the original author 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. + */ + +plugins { + id "java-library" + id "org.springframework.boot.docker-test" + id "org.springframework.boot.auto-configuration" + id "org.springframework.boot.configuration-properties" + id "org.springframework.boot.deployed" + id "org.springframework.boot.optional-dependencies" +} + +description = "Spring Boot support for AMQP 1.0" + +dependencies { + api(project(":core:spring-boot")) + api("org.apache.qpid:protonj2-client") + api("org.springframework:spring-messaging") + api("org.springframework.amqp:spring-amqp-client") + + implementation(project(":module:spring-boot-transaction")) + + optional(project(":core:spring-boot-autoconfigure")) + optional(project(":core:spring-boot-docker-compose")) + optional(project(":module:spring-boot-jackson")) + optional(project(":core:spring-boot-testcontainers")) + optional("org.testcontainers:testcontainers-rabbitmq") + + dockerTestImplementation(project(":core:spring-boot-test")) + dockerTestImplementation(project(":test-support:spring-boot-docker-test-support")) + dockerTestImplementation(testFixtures(project(":core:spring-boot-docker-compose"))) + dockerTestImplementation("ch.qos.logback:logback-classic") + dockerTestImplementation("org.testcontainers:testcontainers-junit-jupiter") + + + testImplementation(project(":core:spring-boot-test")) + testImplementation(project(":test-support:spring-boot-test-support")) + + testRuntimeOnly("ch.qos.logback:logback-classic") +} + +tasks.named("compileTestJava") { + options.nullability.checking = "tests" +} + +tasks.named("compileDockerTestJava") { + options.nullability.checking = "tests" +} diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationIntegrationTests.java new file mode 100644 index 00000000000..d08d29a5598 --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationIntegrationTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-present the original author 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.boot.amqp.autoconfigure; + +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.amqp.client.AmqpClient; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.testsupport.container.RabbitMqManagementContainer; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link AmqpAutoConfiguration}. + * + * @author Stephane Nicoll + */ +@Testcontainers(disabledWithoutDocker = true) +class AmqpAutoConfigurationIntegrationTests { + + @Container + static final RabbitMqManagementContainer container = new RabbitMqManagementContainer(); + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AmqpAutoConfiguration.class)) + .withPropertyValues("spring.amqp.host=" + container.getHost(), "spring.amqp.port=" + container.getAmqpPort()); + + @Test + void sendAndReceiveUsingAmqpClient() { + String queue = container.createRandomQueue(); + this.contextRunner.run((context) -> { + AmqpClient amqpClient = context.getBean(AmqpClient.class); + assertThat(amqpClient.to(queue).body("Hello World").send().get(1, TimeUnit.SECONDS)).isTrue(); + assertThat(amqpClient.from(queue).receiveAndConvert().get(1, TimeUnit.SECONDS)).isEqualTo("Hello World"); + }); + } + + @Test + void sendAndReceiveUsingJson() { + String queue = container.createRandomQueue(); + this.contextRunner.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)).run((context) -> { + AmqpClient amqpClient = context.getBean(AmqpClient.class); + assertThat(amqpClient.to(queue).body(new TestMessage("hello", 42)).send().get(1, TimeUnit.SECONDS)) + .isTrue(); + assertThat(amqpClient.from(queue).receiveAndConvert().get(1, TimeUnit.SECONDS)) + .isEqualTo(new TestMessage("hello", 42)); + }); + } + + record TestMessage(String value, int counter) { + + } + +} diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 00000000000..04198e1e32b --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-present the original author 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.boot.amqp.docker.compose; + +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails; +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails.Address; +import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; +import org.springframework.boot.testsupport.container.TestImage; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link RabbitMqDockerComposeConnectionDetailsFactory}. + * + * @author Stephane Nicoll + */ +class RabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests { + + @DockerComposeTest(composeFile = "rabbitmq-compose.yaml", image = TestImage.RABBITMQ) + void runCreatesConnectionDetails(AmqpConnectionDetails connectionDetails) { + assertThat(connectionDetails.getUsername()).isEqualTo("myuser"); + assertThat(connectionDetails.getPassword()).isEqualTo("secret"); + Address address = connectionDetails.getAddress(); + assertThat(address.host()).isNotNull(); + assertThat(address.port()).isGreaterThan(0); + } + +} diff --git a/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitMqContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitMqContainerConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 00000000000..483ec6e90ad --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitMqContainerConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-present the original author 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.boot.amqp.testcontainers; + +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.amqp.client.AmqpClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.amqp.autoconfigure.AmqpAutoConfiguration; +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.testsupport.container.RabbitMqManagementContainer; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link RabbitMqContainerConnectionDetailsFactory}. + * + * @author Stephane Nicoll + */ +@SpringJUnitConfig +@Testcontainers(disabledWithoutDocker = true) +class RabbitMqContainerConnectionDetailsFactoryIntegrationTests { + + @Container + @ServiceConnection + static final RabbitMqManagementContainer container = new RabbitMqManagementContainer(); + + @Autowired(required = false) + private AmqpConnectionDetails connectionDetails; + + @Autowired + private AmqpClient amqpClient; + + @Test + void connectionCanBeMadeToRabbitMqContainer() throws Exception { + assertThat(this.connectionDetails).isNotNull(); + container.createQueue("test"); + this.amqpClient.to("/queues/test").body("test message").send(); + Object message = this.amqpClient.from("/queues/test").receiveAndConvert().get(4, TimeUnit.MINUTES); + assertThat(message).isEqualTo("test message"); + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(AmqpAutoConfiguration.class) + static class TestConfiguration { + + } + +} diff --git a/module/spring-boot-amqp/src/dockerTest/resources/logback-test.xml b/module/spring-boot-amqp/src/dockerTest/resources/logback-test.xml new file mode 100644 index 00000000000..b8a41480d7d --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/resources/logback-test.xml @@ -0,0 +1,4 @@ + + + + diff --git a/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-compose.yaml b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-compose.yaml new file mode 100644 index 00000000000..1951fba4bb0 --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbitmq-compose.yaml @@ -0,0 +1,8 @@ +services: + rabbitmq: + image: '{imageName}' + environment: + - 'RABBITMQ_DEFAULT_USER=myuser' + - 'RABBITMQ_DEFAULT_PASS=secret' + ports: + - '5672' diff --git a/module/spring-boot-amqp/src/dockerTest/resources/spring.properties b/module/spring-boot-amqp/src/dockerTest/resources/spring.properties new file mode 100644 index 00000000000..47dff33f0bb --- /dev/null +++ b/module/spring-boot-amqp/src/dockerTest/resources/spring.properties @@ -0,0 +1 @@ +spring.test.context.cache.maxSize=1 \ No newline at end of file diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfiguration.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfiguration.java new file mode 100644 index 00000000000..24babf2b488 --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfiguration.java @@ -0,0 +1,132 @@ +/* + * Copyright 2012-present the original author 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.boot.amqp.autoconfigure; + +import java.util.stream.Stream; + +import org.apache.qpid.protonj2.client.Client; +import org.apache.qpid.protonj2.client.ClientOptions; +import org.apache.qpid.protonj2.client.ConnectionOptions; +import tools.jackson.databind.json.JsonMapper; + +import org.springframework.amqp.client.AmqpClient; +import org.springframework.amqp.client.AmqpConnectionFactory; +import org.springframework.amqp.client.SingleAmqpConnectionFactory; +import org.springframework.amqp.support.converter.JacksonJsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails.Address; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Auto-configuration for generic AMQP 1.0 support. + * + * @author Stephane Nicoll + * @since 4.1.0 + */ +@AutoConfiguration(afterName = "org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration") +@ConditionalOnClass({ Client.class, AmqpConnectionFactory.class }) +@EnableConfigurationProperties(AmqpProperties.class) +public final class AmqpAutoConfiguration { + + @Configuration(proxyBeanMethods = false) + static class AmqpConnectionFactoryConfiguration { + + @Bean + @ConditionalOnMissingBean + Client protonClient() { + return Client.create(new ClientOptions()); + } + + @Bean + @ConditionalOnMissingBean + AmqpConnectionDetails amqpConnectionDetails(AmqpProperties properties) { + return new PropertiesAmqpConnectionDetails(properties); + } + + @Bean + @ConditionalOnMissingBean + AmqpConnectionFactory amqpConnectionFactory(AmqpConnectionDetails connectionDetails, Client protonClient, + ObjectProvider connectionOptionsCustomizers) { + ConnectionOptions connectionOptions = createConnectionOptions(connectionDetails, + connectionOptionsCustomizers.orderedStream()); + return createAmqpConnectionFactory(connectionDetails, protonClient).setConnectionOptions(connectionOptions); + } + + private SingleAmqpConnectionFactory createAmqpConnectionFactory(AmqpConnectionDetails connectionDetails, + Client protonClient) { + SingleAmqpConnectionFactory connectionFactory = new SingleAmqpConnectionFactory(protonClient); + PropertyMapper map = PropertyMapper.get(); + Address address = connectionDetails.getAddress(); + map.from(address::host).to(connectionFactory::setHost); + map.from(address::port).to(connectionFactory::setPort); + return connectionFactory; + } + + private ConnectionOptions createConnectionOptions(AmqpConnectionDetails connectionDetails, + Stream customizers) { + ConnectionOptions connectionOptions = new ConnectionOptions(); + PropertyMapper map = PropertyMapper.get(); + map.from(connectionDetails::getUsername).to(connectionOptions::user); + map.from(connectionDetails::getPassword).to(connectionOptions::password); + customizers.forEach((customizer) -> customizer.customize(connectionOptions)); + return connectionOptions; + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnBean(JsonMapper.class) + @ConditionalOnSingleCandidate(JsonMapper.class) + static class JacksonMessageConverterConfiguration { + + @Bean + @ConditionalOnMissingBean(MessageConverter.class) + JacksonJsonMessageConverter jacksonJsonMessageConverter(JsonMapper jsonMapper) { + return new JacksonJsonMessageConverter(jsonMapper); + } + + } + + @Configuration(proxyBeanMethods = false) + static class AmqpClientConfiguration { + + @Bean + @ConditionalOnMissingBean + AmqpClient amqpClient(AmqpConnectionFactory amqpConnectionFactory, AmqpProperties properties, + ObjectProvider messageConverter, ObjectProvider customizers) { + AmqpClient.Builder builder = AmqpClient.builder(amqpConnectionFactory); + AmqpProperties.Client client = properties.getClient(); + PropertyMapper map = PropertyMapper.get(); + map.from(client.getDefaultToAddress()).to(builder::defaultToAddress); + map.from(client.getCompletionTimeout()).to(builder::completionTimeout); + messageConverter.ifAvailable(builder::messageConverter); + customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); + return builder.build(); + } + + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpClientCustomizer.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpClientCustomizer.java new file mode 100644 index 00000000000..b2ab3c2ed76 --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpClientCustomizer.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-present the original author 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.boot.amqp.autoconfigure; + +import org.springframework.amqp.client.AmqpClient; + +/** + * Callback interface that can be used to customize the auto-configured + * {@link AmqpClient.Builder}. + * + * @author Stephane Nicoll + * @since 4.1.0 + */ +@FunctionalInterface +public interface AmqpClientCustomizer { + + /** + * Callback to customize a {@link AmqpClient.Builder} instance. + * @param amqpClientBuilder the client builder to customize + */ + void customize(AmqpClient.Builder amqpClientBuilder); + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpConnectionDetails.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpConnectionDetails.java new file mode 100644 index 00000000000..246183800ae --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpConnectionDetails.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-present the original author 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.boot.amqp.autoconfigure; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; + +/** + * Details required to establish a connection to an AMQP broker. + * + * @author Stephane Nicoll + * @since 4.1.0 + */ +public interface AmqpConnectionDetails extends ConnectionDetails { + + /** + * Return the {@link Address} of the broker. + * @return the address + */ + Address getAddress(); + + /** + * Login user to authenticate to the broker. + * @return the login user to authenticate to the broker or {@code null} + */ + default @Nullable String getUsername() { + return null; + } + + /** + * Password used to authenticate to the broker. + * @return the password to authenticate to the broker or {@code null} + */ + default @Nullable String getPassword() { + return null; + } + + /** + * An AMQP broker address. + * + * @param host the host + * @param port the port + */ + record Address(String host, int port) { + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpProperties.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpProperties.java new file mode 100644 index 00000000000..7091fc29225 --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/AmqpProperties.java @@ -0,0 +1,125 @@ +/* + * Copyright 2012-present the original author 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.boot.amqp.autoconfigure; + +import java.time.Duration; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for AMQP 1.0 connectivity. + * + * @author Stephane Nicoll + * @since 4.1.0 + */ +@ConfigurationProperties("spring.amqp") +public class AmqpProperties { + + /** + * AMQP broker host. + */ + private String host = "localhost"; + + /** + * AMQP broker port. + */ + private int port = 5672; + + /** + * Login user to authenticate to the broker. + */ + private @Nullable String username; + + /** + * Password used to authenticate to the broker. + */ + private @Nullable String password; + + private final Client client = new Client(); + + public String getHost() { + return this.host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return this.port; + } + + public void setPort(int port) { + this.port = port; + } + + public @Nullable String getUsername() { + return this.username; + } + + public void setUsername(@Nullable String username) { + this.username = username; + } + + public @Nullable String getPassword() { + return this.password; + } + + public void setPassword(@Nullable String password) { + this.password = password; + } + + public Client getClient() { + return this.client; + } + + /** + * Client-level settings. + */ + public static class Client { + + /** + * Default destination address for send operations when none is specified. + */ + private @Nullable String defaultToAddress; + + /** + * Maximum time to wait for request operations to complete. + */ + private Duration completionTimeout = Duration.ofSeconds(60); + + public @Nullable String getDefaultToAddress() { + return this.defaultToAddress; + } + + public void setDefaultToAddress(@Nullable String defaultToAddress) { + this.defaultToAddress = defaultToAddress; + } + + public Duration getCompletionTimeout() { + return this.completionTimeout; + } + + public void setCompletionTimeout(Duration completionTimeout) { + this.completionTimeout = completionTimeout; + } + + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionOptionsCustomizer.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionOptionsCustomizer.java new file mode 100644 index 00000000000..9d3c54f1cdb --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/ConnectionOptionsCustomizer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-present the original author 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.boot.amqp.autoconfigure; + +import org.apache.qpid.protonj2.client.ConnectionOptions; + +import org.springframework.amqp.client.AmqpConnectionFactory; + +/** + * Callback interface for customizing {@link ConnectionOptions} on the auto-configured + * {@link AmqpConnectionFactory}. + * + * @author Stephane Nicoll + * @since 4.1.0 + */ +@FunctionalInterface +public interface ConnectionOptionsCustomizer { + + /** + * Customize the {@link ConnectionOptions}. + * @param connectionOptions the connection options to customize + */ + void customize(ConnectionOptions connectionOptions); + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesAmqpConnectionDetails.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesAmqpConnectionDetails.java new file mode 100644 index 00000000000..13e47d047a5 --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesAmqpConnectionDetails.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-present the original author 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.boot.amqp.autoconfigure; + +import org.jspecify.annotations.Nullable; + +/** + * Adapts {@link AmqpProperties} to {@link AmqpConnectionDetails}. + * + * @author Stephane Nicoll + */ +class PropertiesAmqpConnectionDetails implements AmqpConnectionDetails { + + private final AmqpProperties properties; + + PropertiesAmqpConnectionDetails(AmqpProperties properties) { + this.properties = properties; + } + + @Override + public Address getAddress() { + return new Address(this.properties.getHost(), this.properties.getPort()); + } + + @Override + public @Nullable String getUsername() { + return this.properties.getUsername(); + } + + @Override + public @Nullable String getPassword() { + return this.properties.getPassword(); + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java new file mode 100644 index 00000000000..fef2b40e8d3 --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author 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. + */ + +/** + * Auto-configuration for generic AMQP 1.0 support. + */ +@NullMarked +package org.springframework.boot.amqp.autoconfigure; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitMqDockerComposeConnectionDetailsFactory.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitMqDockerComposeConnectionDetailsFactory.java new file mode 100644 index 00000000000..09a45147c4f --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitMqDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,84 @@ +/* + * Copyright 2012-present the original author 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.boot.amqp.docker.compose; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create {@link AmqpConnectionDetails} + * for a {@code rabbitmq} service. + * + * @author Stephane Nicoll + */ +class RabbitMqDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { + + private static final int RABBITMQ_PORT = 5672; + + protected RabbitMqDockerComposeConnectionDetailsFactory() { + super("rabbitmq"); + } + + @Override + protected @Nullable AmqpConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { + try { + return new RabbitMqDockerComposeConnectionDetails(source.getRunningService()); + } + catch (IllegalStateException ex) { + return null; + } + } + + /** + * {@link AmqpConnectionDetails} backed by a {@code rabbitmq} {@link RunningService}. + */ + static class RabbitMqDockerComposeConnectionDetails extends DockerComposeConnectionDetails + implements AmqpConnectionDetails { + + private final RabbitMqEnvironment environment; + + private final Address address; + + protected RabbitMqDockerComposeConnectionDetails(RunningService service) { + super(service); + this.environment = new RabbitMqEnvironment(service.env()); + this.address = new Address(service.host(), service.ports().get(RABBITMQ_PORT)); + } + + @Override + public Address getAddress() { + return this.address; + } + + @Override + public @Nullable String getUsername() { + return this.environment.getUsername(); + } + + @Override + public @Nullable String getPassword() { + return this.environment.getPassword(); + } + + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitMqEnvironment.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitMqEnvironment.java new file mode 100644 index 00000000000..0da210981ae --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitMqEnvironment.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-present the original author 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.boot.amqp.docker.compose; + +import java.util.Map; + +import org.jspecify.annotations.Nullable; + +/** + * RabbitMQ environment details. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @author Scott Frederick + */ +class RabbitMqEnvironment { + + private final @Nullable String username; + + private final @Nullable String password; + + RabbitMqEnvironment(Map env) { + this.username = env.getOrDefault("RABBITMQ_DEFAULT_USER", env.getOrDefault("RABBITMQ_USERNAME", "guest")); + this.password = env.getOrDefault("RABBITMQ_DEFAULT_PASS", env.getOrDefault("RABBITMQ_PASSWORD", "guest")); + } + + @Nullable String getUsername() { + return this.username; + } + + @Nullable String getPassword() { + return this.password; + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java new file mode 100644 index 00000000000..6408a2ff003 --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author 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. + */ + +/** + * Support for Docker Compose generic AMQP service connections. + */ +@NullMarked +package org.springframework.boot.amqp.docker.compose; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitMqContainerConnectionDetailsFactory.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitMqContainerConnectionDetailsFactory.java new file mode 100644 index 00000000000..a36da1c19be --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitMqContainerConnectionDetailsFactory.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-present the original author 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.boot.amqp.testcontainers; + +import java.net.URI; + +import org.testcontainers.rabbitmq.RabbitMQContainer; + +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails; +import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; +import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; + +/** + * {@link ContainerConnectionDetailsFactory} to create {@link AmqpConnectionDetails} from + * a {@link ServiceConnection @ServiceConnection}-annotated {@link RabbitMQContainer}. + * + * @author Stephane Nicoll + */ +class RabbitMqContainerConnectionDetailsFactory + extends ContainerConnectionDetailsFactory { + + @Override + protected AmqpConnectionDetails getContainerConnectionDetails(ContainerConnectionSource source) { + return new RabbitMqContainerConnectionDetails(source); + } + + /** + * {@link AmqpConnectionDetails} for RabbitMQ backed by a + * {@link ContainerConnectionSource}. + */ + static final class RabbitMqContainerConnectionDetails extends ContainerConnectionDetails + implements AmqpConnectionDetails { + + private RabbitMqContainerConnectionDetails(ContainerConnectionSource source) { + super(source); + } + + @Override + public Address getAddress() { + URI uri = URI.create(getContainer().getAmqpUrl()); + return new Address(uri.getHost(), uri.getPort()); + } + + @Override + public String getUsername() { + return getContainer().getAdminUsername(); + } + + @Override + public String getPassword() { + return getContainer().getAdminPassword(); + } + + } + +} diff --git a/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java new file mode 100644 index 00000000000..398ec731847 --- /dev/null +++ b/module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author 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. + */ + +/** + * Support for testcontainers generic AMQP service connections. + */ +@NullMarked +package org.springframework.boot.amqp.testcontainers; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp/src/main/resources/META-INF/spring.factories b/module/spring-boot-amqp/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000000..d3f0d273fc1 --- /dev/null +++ b/module/spring-boot-amqp/src/main/resources/META-INF/spring.factories @@ -0,0 +1,5 @@ +# Connection Details Factories +org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ +org.springframework.boot.amqp.docker.compose.RabbitMqDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.amqp.testcontainers.RabbitMqContainerConnectionDetailsFactory + diff --git a/module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000000..1bbaad209b6 --- /dev/null +++ b/module/spring-boot-amqp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.springframework.boot.amqp.autoconfigure.AmqpAutoConfiguration diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationTests.java b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationTests.java new file mode 100644 index 00000000000..42fe2fa2cef --- /dev/null +++ b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/AmqpAutoConfigurationTests.java @@ -0,0 +1,276 @@ +/* + * Copyright 2012-present the original author 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.boot.amqp.autoconfigure; + +import java.time.Duration; + +import org.apache.qpid.protonj2.client.Client; +import org.apache.qpid.protonj2.client.ConnectionOptions; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; +import tools.jackson.databind.json.JsonMapper; + +import org.springframework.amqp.client.AmqpClient; +import org.springframework.amqp.client.AmqpConnectionFactory; +import org.springframework.amqp.support.converter.JacksonJsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.boot.amqp.autoconfigure.AmqpConnectionDetails.Address; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AmqpAutoConfiguration}. + * + * @author Stephane Nicoll + */ +class AmqpAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AmqpAutoConfiguration.class)); + + @Test + void autoConfigurationConfiguresConnectionFactoryWithDefaultSettings() { + this.contextRunner.run((context) -> { + AmqpConnectionFactory connectionFactory = context.getBean(AmqpConnectionFactory.class); + assertThat(connectionFactory).hasFieldOrPropertyWithValue("host", "localhost"); + assertThat(connectionFactory).hasFieldOrPropertyWithValue("port", 5672); + assertThat(connectionFactory) + .extracting("connectionOptions", InstanceOfAssertFactories.type(ConnectionOptions.class)) + .satisfies((connectOptions) -> { + assertThat(connectOptions).hasFieldOrPropertyWithValue("user", null); + assertThat(connectOptions).hasFieldOrPropertyWithValue("password", null); + }); + }); + } + + @Test + void autoConfigurationConfiguresConnectionFactoryWithOverrides() { + this.contextRunner + .withPropertyValues("spring.amqp.host=amqp.example.com", "spring.amqp.port=1234", + "spring.amqp.username=user", "spring.amqp.password=secret") + .run((context) -> { + AmqpConnectionFactory connectionFactory = context.getBean(AmqpConnectionFactory.class); + assertThat(connectionFactory).hasFieldOrPropertyWithValue("host", "amqp.example.com"); + assertThat(connectionFactory).hasFieldOrPropertyWithValue("port", 1234); + assertThat(connectionFactory) + .extracting("connectionOptions", InstanceOfAssertFactories.type(ConnectionOptions.class)) + .satisfies((connectOptions) -> { + assertThat(connectOptions).hasFieldOrPropertyWithValue("user", "user"); + assertThat(connectOptions).hasFieldOrPropertyWithValue("password", "secret"); + }); + }); + } + + @Test + void autoConfigurationUsesAmqpConnectionDetailsForConnectionFactory() { + AmqpConnectionDetails connectionDetails = mock(); + given(connectionDetails.getAddress()).willReturn(new Address("details.example.com", 9999)); + given(connectionDetails.getUsername()).willReturn("from-details"); + given(connectionDetails.getPassword()).willReturn("details-secret"); + this.contextRunner.withBean(AmqpConnectionDetails.class, () -> connectionDetails) + .withPropertyValues("spring.amqp.host=ignored.example.com", "spring.amqp.port=1111", + "spring.amqp.username=ignored", "spring.amqp.password=ignored") + .run((context) -> { + AmqpConnectionFactory connectionFactory = context.getBean(AmqpConnectionFactory.class); + assertThat(connectionFactory).hasFieldOrPropertyWithValue("host", "details.example.com"); + assertThat(connectionFactory).hasFieldOrPropertyWithValue("port", 9999); + assertThat(connectionFactory) + .extracting("connectionOptions", InstanceOfAssertFactories.type(ConnectionOptions.class)) + .satisfies((connectOptions) -> { + assertThat(connectOptions).hasFieldOrPropertyWithValue("user", "from-details"); + assertThat(connectOptions).hasFieldOrPropertyWithValue("password", "details-secret"); + }); + }); + } + + @Test + void autoConfigurationAppliesConnectionOptionsCustomizer() { + this.contextRunner + .withBean(ConnectionOptionsCustomizer.class, () -> (connectOptions) -> connectOptions.user("custom-user")) + .withPropertyValues("spring.amqp.username=user") + .run((context) -> { + AmqpConnectionFactory connectionFactory = context.getBean(AmqpConnectionFactory.class); + assertThat(connectionFactory) + .extracting("connectionOptions", InstanceOfAssertFactories.type(ConnectionOptions.class)) + .satisfies((connectOptions) -> assertThat(connectOptions).hasFieldOrPropertyWithValue("user", + "custom-user")); + }); + } + + @Test + void autoConfigurationAppliesConnectionOptionsCustomizersInOrder() { + this.contextRunner.withUserConfiguration(ConnectionOptionsCustomizersConfiguration.class).run((context) -> { + ConnectionOptionsCustomizer first = context.getBean("first", ConnectionOptionsCustomizer.class); + ConnectionOptionsCustomizer second = context.getBean("second", ConnectionOptionsCustomizer.class); + InOrder ordered = inOrder(first, second); + ordered.verify(first).customize(any()); + ordered.verify(second).customize(any()); + }); + } + + @Test + void autoConfigurationBacksOffWhenUserProvidesProtonClient() { + Client protonClient = mock(); + this.contextRunner.withBean(Client.class, () -> protonClient).run((context) -> { + assertThat(context).hasSingleBean(Client.class); + assertThat(context.getBean(Client.class)).isSameAs(protonClient); + }); + } + + @Test + void autoConfigurationBacksOffWhenUserProvidesAmqpConnectionDetails() { + AmqpConnectionDetails connectionDetails = mock(); + given(connectionDetails.getAddress()).willReturn(new Address("details.example.com", 9999)); + this.contextRunner.withBean(AmqpConnectionDetails.class, () -> connectionDetails).run((context) -> { + assertThat(context).hasSingleBean(AmqpConnectionDetails.class); + assertThat(context.getBean(AmqpConnectionDetails.class)).isSameAs(connectionDetails); + }); + } + + @Test + void autoConfigurationBacksOffWhenUserProvidesAmqpConnectionFactory() { + AmqpConnectionFactory amqpConnectionFactory = mock(); + this.contextRunner.withBean(AmqpConnectionFactory.class, () -> amqpConnectionFactory).run((context) -> { + assertThat(context).hasSingleBean(AmqpConnectionFactory.class); + assertThat(context.getBean(AmqpConnectionFactory.class)).isSameAs(amqpConnectionFactory); + }); + } + + @Test + void autoConfigurationConfiguresAmqpClientWithOverrides() { + this.contextRunner + .withPropertyValues("spring.amqp.client.default-to-address=/queues/default_queue", + "spring.amqp.client.completion-timeout=20s") + .run((context) -> { + AmqpClient amqpClient = context.getBean(AmqpClient.class); + assertThat(amqpClient).hasFieldOrPropertyWithValue("defaultToAddress", "/queues/default_queue"); + assertThat(amqpClient).hasFieldOrPropertyWithValue("completionTimeout", Duration.ofSeconds(20)); + }); + } + + @Test + void autoConfigurationConfiguresAmqpClientWithJacksonMessageConverter() { + this.contextRunner.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)).run((context) -> { + assertThat(context).hasSingleBean(JsonMapper.class); + AmqpClient amqpClient = context.getBean(AmqpClient.class); + assertThat(amqpClient) + .extracting("messageConverter", InstanceOfAssertFactories.type(MessageConverter.class)) + .satisfies((messageConverter) -> assertThat(messageConverter) + .isInstanceOf(JacksonJsonMessageConverter.class) + .hasFieldOrPropertyWithValue("objectMapper", context.getBean(JsonMapper.class))); + }); + } + + @Test + void autoConfigurationConfiguresAmqpClientWithMessageConverter() { + MessageConverter messageConverter = mock(MessageConverter.class); + this.contextRunner.withBean(MessageConverter.class, () -> messageConverter).run((context) -> { + AmqpClient amqpClient = context.getBean(AmqpClient.class); + assertThat(amqpClient).hasFieldOrPropertyWithValue("messageConverter", messageConverter); + }); + } + + @Test + void autoConfigurationBacksOffWhenUserProvidesAmqpClient() { + AmqpClient amqpClient = mock(); + this.contextRunner.withBean(AmqpClient.class, () -> amqpClient).run((context) -> { + assertThat(context).hasSingleBean(AmqpClient.class); + assertThat(context.getBean(AmqpClient.class)).isSameAs(amqpClient); + }); + } + + @Test + void autoConfigurationUsesUserMessageConverterWhenDefinedAlongsideJackson() { + MessageConverter messageConverter = mock(MessageConverter.class); + this.contextRunner.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class)) + .withBean(MessageConverter.class, () -> messageConverter) + .run((context) -> { + assertThat(context).doesNotHaveBean(JacksonJsonMessageConverter.class); + AmqpClient amqpClient = context.getBean(AmqpClient.class); + assertThat(amqpClient).hasFieldOrPropertyWithValue("messageConverter", messageConverter); + }); + } + + @Test + void autoConfigurationAppliesAmqpClientCustomizer() { + this.contextRunner + .withBean(AmqpClientCustomizer.class, + () -> (client) -> client.defaultToAddress("/queues/custom_default_queue")) + .withPropertyValues("spring.amqp.client.default-to-address=/queues/default_queue") + .run((context) -> { + AmqpClient amqpClient = context.getBean(AmqpClient.class); + assertThat(amqpClient).hasFieldOrPropertyWithValue("defaultToAddress", "/queues/custom_default_queue"); + }); + } + + @Test + void autoConfigurationAppliesAmqpClientCustomizersInOrder() { + this.contextRunner.withUserConfiguration(AmqpClientCustomizersConfiguration.class).run((context) -> { + AmqpClientCustomizer first = context.getBean("first", AmqpClientCustomizer.class); + AmqpClientCustomizer second = context.getBean("second", AmqpClientCustomizer.class); + InOrder ordered = inOrder(first, second); + ordered.verify(first).customize(any()); + ordered.verify(second).customize(any()); + }); + } + + @Configuration(proxyBeanMethods = false) + static class ConnectionOptionsCustomizersConfiguration { + + @Bean + @Order(2) + ConnectionOptionsCustomizer second() { + return mock(); + } + + @Bean + @Order(1) + ConnectionOptionsCustomizer first() { + return mock(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class AmqpClientCustomizersConfiguration { + + @Bean + @Order(2) + AmqpClientCustomizer second() { + return mock(); + } + + @Bean + @Order(1) + AmqpClientCustomizer first() { + return mock(); + } + + } + +} diff --git a/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitMqEnvironmentTests.java b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitMqEnvironmentTests.java new file mode 100644 index 00000000000..01decb6785c --- /dev/null +++ b/module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitMqEnvironmentTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-present the original author 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.boot.amqp.docker.compose; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link RabbitMqEnvironment}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @author Scott Frederick + */ +class RabbitMqEnvironmentTests { + + @Test + void getUsernameWhenNoRabbitMqDefaultUser() { + RabbitMqEnvironment environment = new RabbitMqEnvironment(Collections.emptyMap()); + assertThat(environment.getUsername()).isEqualTo("guest"); + } + + @Test + void getUsernameWhenHasRabbitMqDefaultUser() { + RabbitMqEnvironment environment = new RabbitMqEnvironment(Map.of("RABBITMQ_DEFAULT_USER", "me")); + assertThat(environment.getUsername()).isEqualTo("me"); + } + + @Test + void getUsernameWhenHasRabbitMqUsername() { + RabbitMqEnvironment environment = new RabbitMqEnvironment(Map.of("RABBITMQ_USERNAME", "me")); + assertThat(environment.getUsername()).isEqualTo("me"); + } + + @Test + void getPasswordWhenNoRabbitMqDefaultPass() { + RabbitMqEnvironment environment = new RabbitMqEnvironment(Collections.emptyMap()); + assertThat(environment.getPassword()).isEqualTo("guest"); + } + + @Test + void getPasswordWhenHasRabbitMqDefaultPass() { + RabbitMqEnvironment environment = new RabbitMqEnvironment(Map.of("RABBITMQ_DEFAULT_PASS", "secret")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + + @Test + void getPasswordWhenHasRabbitMqPassword() { + RabbitMqEnvironment environment = new RabbitMqEnvironment(Map.of("RABBITMQ_PASSWORD", "secret")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + +} diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 6519005485b..dd544051fa0 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -1870,6 +1870,19 @@ bom { releaseNotes("https://pulsar.apache.org/release-notes/versioned/pulsar-{version}") } } + library("Qpid ProtonJ2", "1.1.0") { + group("org.apache.qpid") { + modules = [ + "protonj2", + "protonj2-client" + ] + } + links { + site("https://qpid.apache.org/proton") + javadoc("https://qpid.apache.org/releases/qpid-protonj2-{version}/api", "org.apache.qpid.protonj2") + releaseNotes("https://qpid.apache.org/releases/qpid-protonj2-{version}/release-notes.html") + } + } library("Quartz", "2.5.2") { group("org.quartz-scheduler") { modules = [ diff --git a/settings.gradle b/settings.gradle index 5fc768cf572..b7ceb4956e0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -80,6 +80,7 @@ include "core:spring-boot-testcontainers" include "module:spring-boot-activemq" include "module:spring-boot-actuator" include "module:spring-boot-actuator-autoconfigure" +include "module:spring-boot-amqp" include "module:spring-boot-artemis" include "module:spring-boot-autoconfigure-classic" include "module:spring-boot-autoconfigure-classic-modules" @@ -208,6 +209,8 @@ include "starter:spring-boot-starter-activemq" include "starter:spring-boot-starter-activemq-test" include "starter:spring-boot-starter-actuator" include "starter:spring-boot-starter-actuator-test" +include "starter:spring-boot-starter-amqp" +include "starter:spring-boot-starter-amqp-test" include "starter:spring-boot-starter-artemis" include "starter:spring-boot-starter-artemis-test" include "starter:spring-boot-starter-aspectj" diff --git a/starter/spring-boot-starter-amqp-test/build.gradle b/starter/spring-boot-starter-amqp-test/build.gradle new file mode 100644 index 00000000000..6991f36fc91 --- /dev/null +++ b/starter/spring-boot-starter-amqp-test/build.gradle @@ -0,0 +1,26 @@ +/* + * Copyright 2012-present the original author 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. + */ + +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for testing generic AMQP 1.0 support" + +dependencies { + api(project(":starter:spring-boot-starter-amqp")) + api(project(":starter:spring-boot-starter-test")) +} diff --git a/starter/spring-boot-starter-amqp/build.gradle b/starter/spring-boot-starter-amqp/build.gradle new file mode 100644 index 00000000000..39de12fb113 --- /dev/null +++ b/starter/spring-boot-starter-amqp/build.gradle @@ -0,0 +1,27 @@ +/* + * Copyright 2012-present the original author 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. + */ + +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for generic AMQP 1.0 support" + +dependencies { + api(project(":starter:spring-boot-starter")) + + api(project(":module:spring-boot-amqp")) +} diff --git a/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/RabbitMqManagementContainer.java b/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/RabbitMqManagementContainer.java new file mode 100644 index 00000000000..baea0dcddd5 --- /dev/null +++ b/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/RabbitMqManagementContainer.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-present the original author 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.boot.testsupport.container; + +import java.util.UUID; + +import org.testcontainers.rabbitmq.RabbitMQContainer; + +/** + * A {@link RabbitMQContainer} with management plugin enabled. + * + * @author Stephane Nicoll + */ +public class RabbitMqManagementContainer extends RabbitMQContainer { + + public RabbitMqManagementContainer() { + super(TestImage.RABBITMQ.toString()); + } + + /** + * Create a queue with the given name. + * @param name the name of the queue + * @return the AMQP target of the queue + */ + public String createQueue(String name) { + try { + execInContainer("rabbitmqadmin", "queues", "declare", "--name", name); + return "/queues/" + name; + } + catch (Exception ex) { + throw new RuntimeException("Could not create queue", ex); + } + } + + /** + * Create a queue with a random, unique, name. + * @return the AMQP target of the queue + */ + public String createRandomQueue() { + return createQueue(UUID.randomUUID().toString()); + } + +} diff --git a/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/TestImage.java b/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/TestImage.java index e64fc32615c..5f2da85c8b5 100644 --- a/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/TestImage.java +++ b/test-support/spring-boot-docker-test-support/src/main/java/org/springframework/boot/testsupport/container/TestImage.java @@ -245,9 +245,10 @@ public enum TestImage { .withStartupTimeout(Duration.ofMinutes(3))), /** - * A container image suitable for testing RabbitMQ. + * A container image suitable for testing RabbitMQ with support for both AMQP 0.9 and + * 1.0. Queues can be declared using {@code rabbitmqadmin}. */ - RABBITMQ("rabbitmq", "3.11-alpine", () -> RabbitMQContainer.class, + RABBITMQ("rabbitmq", "4.2-management", () -> RabbitMQContainer.class, (container) -> ((RabbitMQContainer) container).withStartupTimeout(Duration.ofMinutes(4))), /**