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))), /**