diff --git a/documentation/spring-boot-docs/build.gradle b/documentation/spring-boot-docs/build.gradle index 88c66a91cce..e1604ce7a37 100644 --- a/documentation/spring-boot-docs/build.gradle +++ b/documentation/spring-boot-docs/build.gradle @@ -95,6 +95,7 @@ dependencies { 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-amqp-rabbitmq")) 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 0194ea8b3d1..f5de88d29eb 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 @@ -1712,17 +1712,17 @@ * xref:reference:io/webservices.adoc#io.webservices[#boot-features-webservices] * xref:reference:io/webservices.adoc#io.webservices[#features.webservices] * xref:reference:io/webservices.adoc#io.webservices[#io.webservices] -* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq[#boot-features-rabbitmq] -* 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.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.rabbitmq09[#boot-features-rabbitmq] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq09[#features.messaging.amqp.rabbit] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq09[#messaging.amqp.rabbit] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq09[#messaging.amqp.rabbitmq09] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq09.receiving[#boot-features-using-amqp-receiving] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq09.receiving[#features.messaging.amqp.receiving] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq09.receiving[#messaging.amqp.receiving] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq09.sending-stream[#messaging.amqp.sending-stream] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq09.sending[#boot-features-using-amqp-sending] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq09.sending[#features.messaging.amqp.sending] +* xref:reference:messaging/amqp.adoc#messaging.amqp.rabbitmq09.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/messaging/amqp.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc index 4aad8ad6290..1718f0abaa7 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,14 @@ 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: 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. +Spring Boot provides auto-configuration for three different AMQP styles: + +* xref:messaging/amqp.adoc#messaging.amqp.generic[Generic AMQP 1.0] (`spring-boot-starter-amqp`) — broker-agnostic AMQP 1.0 support based on the Qpid ProtonJ2 client library. + Use this when portability across AMQP 1.0-compatible brokers such as ActiveMQ, Azure Service Bus, or RabbitMQ is important. +* xref:messaging/amqp.adoc#messaging.amqp.rabbitmq[RabbitMQ AMQP 1.0] (`spring-boot-starter-amqp-rabbitmq`) — RabbitMQ-specific support via the native https://github.com/rabbitmq/rabbitmq-amqp-java-client[RabbitMQ AMQP 1.0 Java client]. + Use this when your application targets RabbitMQ exclusively and you want to use the AMQP 1.0 protocol. +* xref:messaging/amqp.adoc#messaging.amqp.rabbitmq09[RabbitMQ AMQP 0.9] (`spring-boot-starter-rabbitmq`) — RabbitMQ support via the classic AMQP 0.9.1 protocol. + Use this for the most established and feature-rich RabbitMQ integration, including Streams support. @@ -77,9 +84,107 @@ Then you can use the factory in any javadoc:org.springframework.amqp.client.anno include-code::custom/MyBean[] - [[messaging.amqp.rabbitmq]] -== RabbitMQ Support +== RabbitMQ AMQP 1.0 Support + +Spring AMQP provides {url-spring-amqp-docs}/rabbitmq-amqp-client.html[RabbitMQ dedicated support for AMQP 1.0] via `org.springframework.amqp:spring-rabbitmq-client`. + +RabbitMQ AMQP 1.0 configuration is controlled by external configuration properties in `+spring.amqp.rabbitmq.*+`. +For example, you might declare the following section in `application.properties`: + +[configprops,yaml] +---- +spring: + amqp: + rabbitmq: + host: "localhost" + port: 5672 + username: "admin" + password: "secret" +---- + +Alternatively, you could configure the same connection using the `address` attribute: + +[configprops,yaml] +---- +spring: + amqp: + rabbitmq: + address: "amqp://admin:secret@localhost" +---- + +NOTE: When specifying an address that way, the `host` and `port` properties are ignored. +If the address uses the `amqps` scheme, an xref:features/ssl.adoc#features.ssl.bundles[SSL bundle] must be configured. + +See javadoc:org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitProperties[] for more of the supported property-based configuration options. +To configure lower-level details of the auto-configured javadoc:com.rabbitmq.client.amqp.Environment[], define a javadoc:org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpEnvironmentCustomizer[] bean. + +If a javadoc:com.rabbitmq.client.amqp.CredentialsProvider[] bean exists in the context, it will be automatically used by the auto-configured javadoc:com.rabbitmq.client.amqp.Environment[]. + + + +[[messaging.amqp.rabbitmq.ssl]] +=== SSL + +To use SSL with RabbitMQ AMQP 1.0, set configprop:spring.amqp.rabbitmq.ssl.bundle[] to configure the xref:features/ssl.adoc#features.ssl.bundles[SSL bundle] to use. + + + +[[messaging.amqp.rabbitmq.sending]] +=== Sending a Message + +Spring's javadoc:org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate[] and javadoc:org.springframework.amqp.rabbitmq.client.RabbitAmqpAdmin[] are auto-configured, and you can autowire them 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 to the auto-configured javadoc:org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate[]. + +You can set properties for the template as follows: + +[configprops,yaml] +---- +spring: + amqp: + rabbitmq: + template: + exchange: "my-exchange" + routing-key: "my-key" + default-receive-queue: "my-queue" +---- + +To further configure the auto-configured javadoc:org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate[], declare a javadoc:org.springframework.boot.amqp.rabbitmq.autoconfigure.RabbitAmqpTemplateCustomizer[] bean. + + + +[[messaging.amqp.rabbitmq.receiving]] +=== Receiving a Message + +When the RabbitMQ AMQP 1.0 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.rabbitmq.client.config.RabbitAmqpListenerContainerFactory[] has been defined, a default one is automatically configured. +If a javadoc:org.springframework.amqp.support.converter.MessageConverter[] bean is defined, it is associated automatically with the default factory. + +The following sample component creates a listener endpoint on the `someQueue` queue: + +include-code::MyBean[] + +TIP: See javadoc:org.springframework.amqp.rabbit.annotation.EnableRabbit[format=annotation] for more details. + +If you need to create more javadoc:org.springframework.amqp.rabbitmq.client.config.RabbitAmqpListenerContainerFactory[] instances or if you want to override the default, Spring Boot provides a javadoc:org.springframework.boot.amqp.rabbitmq.autoconfigure.RabbitAmqpListenerContainerFactoryConfigurer[] that you can use to initialize a javadoc:org.springframework.amqp.rabbitmq.client.config.RabbitAmqpListenerContainerFactory[] with the same settings as the factory used by the auto-configuration. + +For instance, the following configuration class exposes another factory that uses a specific javadoc:org.springframework.amqp.support.converter.MessageConverter[]: + +include-code::custom/MyRabbitAmqpConfiguration[] + +Then you can use the factory in any javadoc:org.springframework.amqp.rabbit.annotation.RabbitListener[format=annotation]-annotated method, as follows: + +include-code::custom/MyBean[] + +To customize the listener container, define a javadoc:org.springframework.amqp.rabbit.config.ContainerCustomizer[] bean parameterized with javadoc:org.springframework.amqp.rabbitmq.client.listener.RabbitAmqpListenerContainer[]. + + + +[[messaging.amqp.rabbitmq09]] +== RabbitMQ AMQP 0.9 Support https://www.rabbitmq.com/[RabbitMQ] is a lightweight, reliable, scalable, and portable message broker based on the AMQP protocol. Spring uses RabbitMQ to communicate through the AMQP protocol. @@ -120,7 +225,7 @@ TIP: See https://spring.io/blog/2010/06/14/understanding-amqp-the-protocol-used- -[[messaging.amqp.rabbitmq.sending]] +[[messaging.amqp.rabbitmq09.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: @@ -153,7 +258,7 @@ If there's a bean of type javadoc:org.springframework.amqp.rabbit.support.microm -[[messaging.amqp.rabbitmq.sending-stream]] +[[messaging.amqp.rabbitmq09.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: @@ -172,13 +277,13 @@ If you need to create more javadoc:org.springframework.rabbit.stream.producer.Ra -[[messaging.amqp.rabbitmq.sending-stream.ssl]] +[[messaging.amqp.rabbitmq09.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.rabbitmq.receiving]] +[[messaging.amqp.rabbitmq09.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. diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.java index 56064bfb94c..1d224b15829 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.java @@ -16,13 +16,15 @@ package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving; +import org.springframework.amqp.rabbit.annotation.RabbitHandler; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; @Component +@RabbitListener(queues = "someQueue") public class MyBean { - @RabbitListener(queues = "someQueue") + @RabbitHandler public void processMessage(String content) { // ... } diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitAmqpConfiguration.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitAmqpConfiguration.java new file mode 100644 index 00000000000..ce801674115 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitAmqpConfiguration.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.rabbitmq.receiving.custom; + +import org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory; +import org.springframework.amqp.rabbitmq.client.config.RabbitAmqpListenerContainerFactory; +import org.springframework.boot.amqp.rabbitmq.autoconfigure.RabbitAmqpListenerContainerFactoryConfigurer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +public class MyRabbitAmqpConfiguration { + + @Bean + public RabbitAmqpListenerContainerFactory myFactory(RabbitAmqpListenerContainerFactoryConfigurer configurer, + AmqpConnectionFactory connectionFactory) { + RabbitAmqpListenerContainerFactory factory = new RabbitAmqpListenerContainerFactory(connectionFactory); + configurer.configure(factory); + factory.setMessageConverter(new MyMessageConverter()); + return factory; + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.java index 51b91d35769..90811489713 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.java @@ -16,27 +16,23 @@ package org.springframework.boot.docs.messaging.amqp.rabbitmq.sending; -import org.springframework.amqp.core.AmqpAdmin; -import org.springframework.amqp.core.AmqpTemplate; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpAdmin; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; import org.springframework.stereotype.Component; @Component public class MyBean { - private final AmqpAdmin amqpAdmin; + private final RabbitAmqpAdmin amqpAdmin; - private final AmqpTemplate amqpTemplate; + private final RabbitAmqpTemplate amqpTemplate; - public MyBean(AmqpAdmin amqpAdmin, AmqpTemplate amqpTemplate) { + public MyBean(RabbitAmqpAdmin amqpAdmin, RabbitAmqpTemplate amqpTemplate) { this.amqpAdmin = amqpAdmin; this.amqpTemplate = amqpTemplate; } // @fold:on // ... - public void someMethod() { - this.amqpAdmin.getQueueInfo("someQueue"); - } - public void someOtherMethod() { this.amqpTemplate.convertAndSend("hello"); } diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/MyBean.java new file mode 100644 index 00000000000..6d44d39ec2e --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/MyBean.java @@ -0,0 +1,30 @@ +/* + * 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.rabbitmq09.receiving; + +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + @RabbitListener(queues = "someQueue") + public void processMessage(String content) { + // ... + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyBean.java new file mode 100644 index 00000000000..35031b330cc --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyBean.java @@ -0,0 +1,30 @@ +/* + * 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.rabbitmq09.receiving.custom; + +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + @RabbitListener(queues = "someQueue", containerFactory = "myFactory") + public void processMessage(String content) { + // ... + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyMessageConverter.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyMessageConverter.java new file mode 100644 index 00000000000..e63133f58d0 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyMessageConverter.java @@ -0,0 +1,36 @@ +/* + * 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.rabbitmq09.receiving.custom; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.support.converter.MessageConversionException; +import org.springframework.amqp.support.converter.MessageConverter; + +class MyMessageConverter implements MessageConverter { + + @Override + public Message toMessage(Object object, MessageProperties messageProperties) throws MessageConversionException { + return null; + } + + @Override + public Object fromMessage(Message message) throws MessageConversionException { + return null; + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyRabbitConfiguration.java similarity index 95% rename from documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.java rename to documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyRabbitConfiguration.java index 318428b08be..f47e0d4d1f9 100644 --- a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.java +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyRabbitConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving.custom; +package org.springframework.boot.docs.messaging.amqp.rabbitmq09.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/rabbitmq09/sending/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/sending/MyBean.java new file mode 100644 index 00000000000..a4247919fc6 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmq09/sending/MyBean.java @@ -0,0 +1,45 @@ +/* + * 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.rabbitmq09.sending; + +import org.springframework.amqp.core.AmqpAdmin; +import org.springframework.amqp.core.AmqpTemplate; +import org.springframework.stereotype.Component; + +@Component +public class MyBean { + + private final AmqpAdmin amqpAdmin; + + private final AmqpTemplate amqpTemplate; + + public MyBean(AmqpAdmin amqpAdmin, AmqpTemplate amqpTemplate) { + this.amqpAdmin = amqpAdmin; + this.amqpTemplate = amqpTemplate; + } + + // @fold:on // ... + public void someMethod() { + this.amqpAdmin.getQueueInfo("someQueue"); + } + + public void someOtherMethod() { + this.amqpTemplate.convertAndSend("hello"); + } + // @fold:off + +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.kt index 9bac053496d..a477cb2d84c 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/MyBean.kt @@ -16,17 +16,18 @@ package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving +import org.springframework.amqp.rabbit.annotation.RabbitHandler import org.springframework.amqp.rabbit.annotation.RabbitListener import org.springframework.stereotype.Component @Suppress("UNUSED_PARAMETER") @Component +@RabbitListener(queues = ["someQueue"]) class MyBean { - @RabbitListener(queues = ["someQueue"]) + @RabbitHandler fun processMessage(content: String?) { // ... } } - diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.kt index 3b90bf3dc42..41e3ab585b0 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyBean.kt @@ -29,4 +29,3 @@ class MyBean { } } - diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.kt index 03b165448e0..971452683e7 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyMessageConverter.kt @@ -31,4 +31,3 @@ internal class MyMessageConverter : MessageConverter { } } - diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitAmqpConfiguration.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitAmqpConfiguration.kt new file mode 100644 index 00000000000..95249b2409c --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitAmqpConfiguration.kt @@ -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.docs.messaging.amqp.rabbitmq.receiving.custom + +import org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory +import org.springframework.amqp.rabbitmq.client.config.RabbitAmqpListenerContainerFactory +import org.springframework.boot.amqp.rabbitmq.autoconfigure.RabbitAmqpListenerContainerFactoryConfigurer +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration(proxyBeanMethods = false) +class MyRabbitAmqpConfiguration { + + @Bean + fun myFactory( + configurer: RabbitAmqpListenerContainerFactoryConfigurer, + connectionFactory: AmqpConnectionFactory + ): RabbitAmqpListenerContainerFactory { + val factory = RabbitAmqpListenerContainerFactory(connectionFactory) + configurer.configure(factory) + factory.setMessageConverter(MyMessageConverter()) + return factory + } + +} diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.kt index 6d21de9bce0..cf1223d7105 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/sending/MyBean.kt @@ -16,22 +16,17 @@ package org.springframework.boot.docs.messaging.amqp.rabbitmq.sending -import org.springframework.amqp.core.AmqpAdmin -import org.springframework.amqp.core.AmqpTemplate +import org.springframework.amqp.rabbitmq.client.RabbitAmqpAdmin +import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate import org.springframework.stereotype.Component @Component -class MyBean(private val amqpAdmin: AmqpAdmin, private val amqpTemplate: AmqpTemplate) { +class MyBean(private val amqpAdmin: RabbitAmqpAdmin, private val amqpTemplate: RabbitAmqpTemplate) { // @fold:on // ... - fun someMethod() { - amqpAdmin.getQueueInfo("someQueue") - } - fun someOtherMethod() { amqpTemplate.convertAndSend("hello") } // @fold:off } - diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/MyBean.kt new file mode 100644 index 00000000000..99244d15b01 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/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.rabbitmq09.receiving + +import org.springframework.amqp.rabbit.annotation.RabbitListener +import org.springframework.stereotype.Component + +@Suppress("UNUSED_PARAMETER") +@Component +class MyBean { + + @RabbitListener(queues = ["someQueue"]) + fun processMessage(content: String?) { + // ... + } + +} + diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyBean.kt new file mode 100644 index 00000000000..b119e83ccdf --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/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.rabbitmq09.receiving.custom + +import org.springframework.amqp.rabbit.annotation.RabbitListener +import org.springframework.stereotype.Component + +@Suppress("UNUSED_PARAMETER") +@Component +class MyBean { + + @RabbitListener(queues = ["someQueue"], containerFactory = "myFactory") + fun processMessage(content: String?) { + // ... + } + +} + diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyMessageConverter.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyMessageConverter.kt new file mode 100644 index 00000000000..0005bbb328e --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyMessageConverter.kt @@ -0,0 +1,34 @@ +/* + * 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.rabbitmq09.receiving.custom + +import org.springframework.amqp.core.Message +import org.springframework.amqp.core.MessageProperties +import org.springframework.amqp.support.converter.MessageConverter + +internal class MyMessageConverter : MessageConverter { + + override fun toMessage(`object`: Any, messageProperties: MessageProperties): Message { + return Message(byteArrayOf()) + } + + override fun fromMessage(message: Message): Any { + return Any() + } + +} + diff --git a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyRabbitConfiguration.kt similarity index 95% rename from documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.kt rename to documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyRabbitConfiguration.kt index 900866bf246..17d5ced6dd0 100644 --- a/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq/receiving/custom/MyRabbitConfiguration.kt +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/receiving/custom/MyRabbitConfiguration.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.docs.messaging.amqp.rabbitmq.receiving.custom +package org.springframework.boot.docs.messaging.amqp.rabbitmq09.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/rabbitmq09/sending/MyBean.kt b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/sending/MyBean.kt new file mode 100644 index 00000000000..07dbdff26ad --- /dev/null +++ b/documentation/spring-boot-docs/src/main/kotlin/org/springframework/boot/docs/messaging/amqp/rabbitmq09/sending/MyBean.kt @@ -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.rabbitmq09.sending + +import org.springframework.amqp.core.AmqpAdmin +import org.springframework.amqp.core.AmqpTemplate +import org.springframework.stereotype.Component + +@Component +class MyBean(private val amqpAdmin: AmqpAdmin, private val amqpTemplate: AmqpTemplate) { + + // @fold:on // ... + fun someMethod() { + amqpAdmin.getQueueInfo("someQueue") + } + + fun someOtherMethod() { + amqpTemplate.convertAndSend("hello") + } + // @fold:off + +} + diff --git a/module/spring-boot-amqp-rabbitmq/build.gradle b/module/spring-boot-amqp-rabbitmq/build.gradle new file mode 100644 index 00000000000..827a5f4a755 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/build.gradle @@ -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. + */ + +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 AMQP 1.0 support for RabbitMQ" + +dependencies { + api(project(":core:spring-boot")) + api("org.springframework:spring-messaging") + api("org.springframework.amqp:spring-rabbitmq-client") + + compileOnly("com.fasterxml.jackson.core:jackson-annotations") + + implementation(project(":module:spring-boot-transaction")) + + optional(project(":core:spring-boot-autoconfigure")) + optional(project(":core:spring-boot-docker-compose")) + optional(project(":core:spring-boot-testcontainers")) + optional(project(":module:spring-boot-health")) + optional("org.testcontainers:testcontainers-rabbitmq") + + 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") + + testCompileOnly("com.fasterxml.jackson.core:jackson-annotations") + + 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-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/rabbitmq/docker/compose/AmqpRabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/rabbitmq/docker/compose/AmqpRabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 00000000000..3a4cfbd8b2d --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/rabbitmq/docker/compose/AmqpRabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests.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.rabbitmq.docker.compose; + +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitConnectionDetails; +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitConnectionDetails.Address; +import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.testsupport.container.TestImage; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link AmqpRabbitMqDockerComposeConnectionDetailsFactory}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @author Scott Frederick + * @author Eddú Meléndez + */ +class AmqpRabbitMqDockerComposeConnectionDetailsFactoryIntegrationTests { + + @DockerComposeTest(composeFile = "rabbitmq-compose.yaml", image = TestImage.RABBITMQ) + void runCreatesConnectionDetails(AmqpRabbitConnectionDetails connectionDetails) { + assertConnectionDetails(connectionDetails); + assertThat(connectionDetails.getSslBundle()).isNull(); + } + + @DockerComposeTest(composeFile = "rabbitmq-ssl-compose.yaml", image = TestImage.RABBITMQ, + additionalResources = { "../../ca.crt", "../../server.crt", "../../server.key", "../../client.crt", + "../../client.key", "rabbitmq-ssl.conf" }) + void runWithSslCreatesConnectionDetails(AmqpRabbitConnectionDetails connectionDetails) { + assertConnectionDetails(connectionDetails); + SslBundle sslBundle = connectionDetails.getSslBundle(); + assertThat(sslBundle).isNotNull(); + } + + private void assertConnectionDetails(AmqpRabbitConnectionDetails connectionDetails) { + assertThat(connectionDetails.getUsername()).isEqualTo("myuser"); + assertThat(connectionDetails.getPassword()).isEqualTo("secret"); + assertThat(connectionDetails.getVirtualHost()).isEqualTo("/"); + Address address = connectionDetails.getAddress(); + assertThat(address.host()).isNotNull(); + assertThat(address.port()).isGreaterThan(0); + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/rabbitmq/testcontainers/AmqpRabbitMqContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/rabbitmq/testcontainers/AmqpRabbitMqContainerConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 00000000000..cbab101ebca --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/rabbitmq/testcontainers/AmqpRabbitMqContainerConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,99 @@ +/* + * 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.rabbitmq.testcontainers; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.rabbitmq.RabbitMQContainer; + +import org.springframework.amqp.rabbit.annotation.Queue; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitAutoConfiguration; +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitConnectionDetails; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.testsupport.container.TestImage; +import org.springframework.context.annotation.Bean; +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 AmqpRabbitMqContainerConnectionDetailsFactory}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @author Eddú Meléndez + */ +@SpringJUnitConfig +@Testcontainers(disabledWithoutDocker = true) +class AmqpRabbitMqContainerConnectionDetailsFactoryIntegrationTests { + + @Container + @ServiceConnection + static final RabbitMQContainer rabbit = TestImage.container(RabbitMQContainer.class); + + @Autowired(required = false) + private AmqpRabbitConnectionDetails connectionDetails; + + @Autowired + private RabbitAmqpTemplate rabbitAmqpTemplate; + + @Autowired + private TestListener listener; + + @Test + void connectionCanBeMadeToRabbitContainer() { + assertThat(this.connectionDetails).isNotNull(); + this.rabbitAmqpTemplate.convertAndSend("test", "message"); + Awaitility.waitAtMost(Duration.ofMinutes(4)) + .untilAsserted(() -> assertThat(this.listener.messages).containsExactly("message")); + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(AmqpRabbitAutoConfiguration.class) + static class TestConfiguration { + + @Bean + TestListener testListener() { + return new TestListener(); + } + + } + + static class TestListener { + + private final List messages = new ArrayList<>(); + + @RabbitListener(queuesToDeclare = @Queue("test")) + void processMessage(String message) { + this.messages.add(message); + } + + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/rabbitmq/testcontainers/AmqpRabbitMqContainerWithSslConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/rabbitmq/testcontainers/AmqpRabbitMqContainerWithSslConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 00000000000..9517219297e --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/rabbitmq/testcontainers/AmqpRabbitMqContainerWithSslConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,124 @@ +/* + * 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.rabbitmq.testcontainers; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.rabbitmq.RabbitMQContainer; +import org.testcontainers.utility.MountableFile; + +import org.springframework.amqp.rabbit.annotation.Queue; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitAutoConfiguration; +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitConnectionDetails; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.testcontainers.service.connection.PemKeyStore; +import org.springframework.boot.testcontainers.service.connection.PemTrustStore; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.testsupport.container.TestImage; +import org.springframework.context.annotation.Bean; +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 AmqpRabbitMqContainerConnectionDetailsFactory} with SSL. + * + * @author Stephane Nicoll + */ +@SpringJUnitConfig +@Testcontainers(disabledWithoutDocker = true) +class AmqpRabbitMqContainerWithSslConnectionDetailsFactoryIntegrationTests { + + private static final int RABBITMQ_TLS_PORT = 5671; + + @Container + @ServiceConnection + @PemTrustStore(certificate = "classpath:org/springframework/boot/amqp/rabbitmq/ca.crt") + @PemKeyStore(certificate = "classpath:org/springframework/boot/amqp/rabbitmq/client.crt", + privateKey = "classpath:org/springframework/boot/amqp/rabbitmq/client.key") + static final RabbitMQContainer rabbit = getRabbitMqContainer(); + + private static RabbitMQContainer getRabbitMqContainer() { + RabbitMQContainer container = TestImage.container(RabbitMQContainer.class); + container.addExposedPorts(RABBITMQ_TLS_PORT); + container.withCopyFileToContainer( + MountableFile + .forClasspathResource("org/springframework/boot/amqp/rabbitmq/testcontainers/rabbitmq-ssl.conf"), + "/etc/rabbitmq/rabbitmq.conf"); + container.withCopyFileToContainer( + MountableFile.forClasspathResource("org/springframework/boot/amqp/rabbitmq/ca.crt"), + "/etc/rabbitmq/ca.crt"); + container.withCopyFileToContainer( + MountableFile.forClasspathResource("org/springframework/boot/amqp/rabbitmq/server.key"), + "/etc/rabbitmq/server.key"); + container.withCopyFileToContainer( + MountableFile.forClasspathResource("org/springframework/boot/amqp/rabbitmq/server.crt"), + "/etc/rabbitmq/server.crt"); + return container; + } + + @Autowired(required = false) + private AmqpRabbitConnectionDetails connectionDetails; + + @Autowired + private RabbitAmqpTemplate rabbitAmqpTemplate; + + @Autowired + private TestListener listener; + + @Test + void connectionCanBeMadeToRabbitContainerWithSsl() { + assertThat(this.connectionDetails).isNotNull(); + assertThat(this.connectionDetails.getSslBundle()).isNotNull(); + this.rabbitAmqpTemplate.convertAndSend("test", "message"); + Awaitility.waitAtMost(Duration.ofMinutes(4)) + .untilAsserted(() -> assertThat(this.listener.messages).containsExactly("message")); + } + + @Configuration(proxyBeanMethods = false) + @ImportAutoConfiguration(AmqpRabbitAutoConfiguration.class) + static class TestConfiguration { + + @Bean + TestListener testListener() { + return new TestListener(); + } + + } + + static class TestListener { + + private final List messages = new ArrayList<>(); + + @RabbitListener(queuesToDeclare = @Queue("test")) + void processMessage(String message) { + this.messages.add(message); + } + + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/logback-test.xml b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/logback-test.xml new file mode 100644 index 00000000000..b8a41480d7d --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/logback-test.xml @@ -0,0 +1,4 @@ + + + + diff --git a/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/ca.crt b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/ca.crt new file mode 100644 index 00000000000..beed250b132 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/ca.crt @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFhjCCA26gAwIBAgIUfIkk29IT9OpbgfjL8oRIPSLjUcAwDQYJKoZIhvcNAQEL +BQAwOzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MB4XDTI0MDUwMTE2NTMyNVoXDTM0MDQyOTE2NTMyNVow +OzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlmaWNh +dGUgQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAusN2 +KzQQUUxZSiI3ZZuZohFwq2KXSUNPdJ6rgD3/YKNTDSZXKZPO53kYPP0DXf0sm3CH +cyWSWVabyimZYuPWena1MElSL4ZpJ9WwkZoOQ3bPFK1utz6kMOwrgAUcky8H/rIK +j2JEBhkSHUIGr57NjUEwG1ygaSerM8RzWw1PtMq+C8LOu3v94qzE3NDg1QRpyvV9 +OmsLsjISd0ZmAJNi9vmiEH923KnPyiqnQmWKpYicdgQmX1GXylS22jZqAwaOkYGj +X8UdeyvrohkZkM0hn9uaSufQGEW4yKACn3PkjJtzi8drBIyjIi9YcAzBxZB9oVKq +XZMlltgO2fDMmIJi0Ngt0Ci7fCoEMqSocKyDKML6YLr9UWtx4bfsrk+rVO9Q/D/v +8RKgstv7dCf2KWRX3ZJEC0IBHS5gLNq0qqqVcGx3LcSyhdiKJOtSwAnNkHMh+jSQ +xLSlBjcSqTPiGTRK/Rddl+xnU/mBgk7ZBGNrUFaD5McMFjddS7Ih82aHnpQ1gekW +nUGv+Tm/G68h2BvZ5U2q+RfeOCgRW9i/AYW2jgT7IFnfjyUXgBQveauMAchomqFE +VLe95ZgViF6vmH34EKo3w9L5TQiwk/r53YlM7TSOTyDqx66t4zGYDsVMicpKmzi4 +2Rp8EpErARRyREUIKSvWs9O9+uT3+7arNLgHe5ECAwEAAaOBgTB/MB0GA1UdDgQW +BBRVMLDVqPECWaH6GruL9E52VcTrPjAfBgNVHSMEGDAWgBRVMLDVqPECWaH6GruL +9E52VcTrPjAPBgNVHRMBAf8EBTADAQH/MCwGA1UdEQQlMCOCC2V4YW1wbGUuY29t +gglsb2NhbGhvc3SCCTEyNy4wLjAuMTANBgkqhkiG9w0BAQsFAAOCAgEAeSpjCL3j +2GIFBNKr/5amLOYa0kZ6r1dJs+K6xvMsUvsBJ/QQsV5nYDMIoV/NYUd8SyYV4lEj +7LHX5ZbmJrvPk30LGEBG/5Vy2MIATrQrQ14S4nXtEdSnBvTQwPOOaHc+2dTp3YpM +f4ffELKWyispTifx1eqdiUJhURKeQBh+3W7zpyaiN4vJaqEDKGgFQtHA/OyZL2hZ +BpxHB0zpb2iDHV8MeyfOT7HQWUk6p13vdYm6EnyJT8fzWvE+TqYNbqFmB+CLRSXy +R3p1yaeTd4LnVknJ0UBKqEyul3ziHZDhKhBpwdglYOQz4eWjSFhikX9XZ8NaI38Q +QqLZVn0DsH2ztkjrQrUVgK2xn4aUuqoLDk4Hu6h5baUn+f2GLuzx+EXc/i3ikYvw +Y3JyufOgw6nGGFG+/QXEj85XtLPhN7Wm42z2e/BGzi0MLl65sfpEDXvFTA72Yzws +OYaeg/HxeYwUHQgs2fKl/LgV4chntSCvTqfNl6OnQafD/ISJNpx3xWR3HwF+ypFG +UaLE+e1soqEJbzL31U/6pypHLsj8Y8r9hJbZXo2ibnhjFV6fypUAP0rbIzaoWcrJ +T0Sbliz+KQTMzCcubiAi4bI/kZ5FJ4kkaHqUpIWzlx1h2WVJ65ASFDjBWb8eVmB6 +Dyno/RVFR/rUL5091gjGRXhLsi1oUHKdEzU= +-----END CERTIFICATE----- diff --git a/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/client.crt b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/client.crt new file mode 100644 index 00000000000..811d880fcbd --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/client.crt @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEWjCCAkKgAwIBAgIURBZvq442tp+/K9TZII5Vy/LzVx0wDQYJKoZIhvcNAQEL +BQAwOzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MB4XDTI0MDUwMTE2NTMyNVoXDTM0MDQyOTE2NTMyNVow +LzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDESMBAGA1UEAwwJbG9jYWxob3N0 +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvGb7tu0odSuOjeY1lHlh +sRR4PayAvlryjfrrp49hjoVTiL3d/Jo6Po5HlqwJcYuclm0EWQR5Vur/zYJpfUE7 +b8+E9Qwe50+YzfQ2tVFEdq/VfqemrYRGee+pMelOCI90enOKCxfpo6EHbz+WnUP0 +mnD8OAF9QpolSdWAMOGJoPdWX65KQvyMXvQbj9VIHmsx7NCaIOYxjHXB/dI2FmXV ++m4VT6mb8he9dXmgK/ozMq6XIPOAXe0n3dlfMTSEddeNeVwnBpr/n5e0cpwGFhdf +NNu5CI4ecipBhXljJi/4/47M/6hd69HwE05C4zyH4ZDZ2JTfaSKOLV+jYdBUqJP5 +dwIDAQABo2IwYDALBgNVHQ8EBAMCBaAwEQYJYIZIAYb4QgEBBAQDAgeAMB0GA1Ud +DgQWBBRWiWOo9cm2IF/ZlhWLVjifLzYa/DAfBgNVHSMEGDAWgBRVMLDVqPECWaH6 +GruL9E52VcTrPjANBgkqhkiG9w0BAQsFAAOCAgEAA5Wphtu2nBhY+QNOBOwXq4zF +N5qt2IYTLfR7xqpKhhXx9VkIjdPWpcsGuCuMmfPVNvQWE6iK0/jMMqToTj4H6K7e +MN74j0GwwcknT1P42tUzEpg8LKR8VMdhWhyqdniCDNWWuaz1iVSoF0S2i4jFSzH5 +1q3KMKMZ4niK5aJI0fAGa4fCjyuun1Mfg/qGBGwLnqDkIXjeAopZf4Jb64TtzjAs +j9NT6mYbe3E0tw3fHT9ihYdbZDZgSjeCsuq9OiRMVb0DWWmRoLmmOrlN8IJlHV/3 +WyI/ta4Cw5EZ0oaOg0lIyOxXyvElth1xIvh+kdqZSBsU0gNBri6ZIzYbbTh2KTTO +BJHQt9L5naWG27pDrIxBicWXS/MIYonktm3YgCLfuW3kWcVk8bIlNhfcoAYBBgfM +IEYSYEq+bH2IQ+YoWQz3AxjJ8gEuuSUP6R6mYY65FfpjkKgcpGBvw4EIAmqKDtPS +hlLY/F0XVj9KZzrMyH4/vonu+DAb/P7Zmt2fyk/dQO6bAc3ltRmJbJm4VJ2v/T8I +LVu2FtcUYgtLNtkWUPfdb3GSUUgkKlUpWSty31TKSUszJjW1oRykQhEko6o5U3S8 +ptQzXdApsb1lGOqewkubE25tIu2RLiNkKcjFOjJ/lu0vP9k76wWwRVnFLFvfo4lW +pgywiOifs5JbcCt0ZQ0= +-----END CERTIFICATE----- diff --git a/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/client.key b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/client.key new file mode 100644 index 00000000000..2ae0f49bf4a --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/client.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC8Zvu27Sh1K46N +5jWUeWGxFHg9rIC+WvKN+uunj2GOhVOIvd38mjo+jkeWrAlxi5yWbQRZBHlW6v/N +gml9QTtvz4T1DB7nT5jN9Da1UUR2r9V+p6athEZ576kx6U4Ij3R6c4oLF+mjoQdv +P5adQ/SacPw4AX1CmiVJ1YAw4Ymg91ZfrkpC/Ixe9BuP1UgeazHs0Jog5jGMdcH9 +0jYWZdX6bhVPqZvyF711eaAr+jMyrpcg84Bd7Sfd2V8xNIR11415XCcGmv+fl7Ry +nAYWF18027kIjh5yKkGFeWMmL/j/jsz/qF3r0fATTkLjPIfhkNnYlN9pIo4tX6Nh +0FSok/l3AgMBAAECggEABXnBe3MwXAMQENzNypOiXK4VE3XMYkePfdsSK163byOD +w3ZeTgQNfU4g8LJK8/homzO0SQIJAdz2+ZFbpsp4A2W2zJ+1jvN5RuX/8/UcVhmk +tb1IL/LWCvx5/aoYBWkgIA70UfQJa2jDbdM0v5j/Gu9yE7GI14jh6DFC3xGMGV3b +fOwManxf7sDibCI1nGjnFYNGxninRr+tpb+a1KNbVzhett68LrgPmtph6B3HCPAJ +zBigk1Phgb8WHozTXxnLyw9/RdKJ0Ro4PFmtQv0EvCSlytptnF+0nXkqr3f851XS +bUWwYFchIFWPMhPfD5B3niNWCV42/sU/bQlk+BMQAQKBgQD6NvMq8EdYy2Y7fXT5 +FgB4s+7EkLgI2d5LUaCXCFgc6iZtCTQKUXj1rIWeRfGrFVCCe8qV+XIMKt/G5eEi +tn5ifHhktA2A8GK1scj026qHP3bVn0hMaUnkCF1UpDRKPiEO5G/apPtav8PbCNaX +GAimLGw+WZNZuv7+T33bEBeUdwKBgQDAwiidayLXkRkz2deefdDKcXQsB7RHFGGy +vfZPBCGqizxml+6ojJkkDsVUKL1IXFfyK9KpQAI6tezn4oktgu4jAQqkYY7QZobs +RpQx1dR+KxEm7ISDBTq/B1Q9cFKUKVvQQy8N2pnIbCdzb6MTOKLmJqFGTjr+5T8q +F32B5vkDAQKBgDCKfH42AwFc5EZiPlEcTZcdARMtKCa/bXqbKVZjjgR+AFpi0K+3 +womWoI1l8E5KYkYOEe0qaU+m+aaybgy37qjYkNqoe34qJFwvU1b9ToXScBFdRz9b +pbQRU1naSTKl/u/OrUxzeTfPwAU8H7VMOlFSiOVHp2he+J0JetcGtixdAoGBAIJQ +QMj7rxhxHcqyEVUy1b6nKNTDeJs9Kjd+uU/+CQyVCQaK3GvScY2w9rLIv/51f3dX +LRoDDf7HExxJSFgeVgQQJjOvSK+XQMvngzSVzQxm7TeVWpiBJpAS0l6e2xUTSODp +KpyBFsoqZBlkdaj+9xIFN66iILxGG4fHTbBOiDYBAoGBAOZMKjM5N/hGcCmik/6t +p/zBA2pN9O6zwPndITTsdyVWSlVqCZhXlRX47CerAN+/WVCidlh7Vp5Tuy75Wa77 +v16IDLO01txgWNobcLaM4VgFsyLi5JuxK73S18Vb1cKWdHFRF0LH3cUIq20fjpv6 +Odl4vjNOncXMZCLPHQ+bKWaf +-----END PRIVATE KEY----- diff --git a/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/docker/compose/rabbitmq-compose.yaml b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/docker/compose/rabbitmq-compose.yaml new file mode 100644 index 00000000000..1951fba4bb0 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/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-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/docker/compose/rabbitmq-ssl-compose.yaml b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/docker/compose/rabbitmq-ssl-compose.yaml new file mode 100644 index 00000000000..69179aa1efe --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/docker/compose/rabbitmq-ssl-compose.yaml @@ -0,0 +1,26 @@ +services: + rabbitmq: + image: '{imageName}' + environment: + - 'RABBITMQ_DEFAULT_USER=myuser' + - 'RABBITMQ_DEFAULT_PASS=secret' + ports: + - '5672' + - '5671' + secrets: + - ssl-ca + - ssl-key + - ssl-cert + volumes: + - ./rabbitmq-ssl.conf:/etc/rabbitmq/rabbitmq.conf:ro + labels: + - 'org.springframework.boot.sslbundle.pem.keystore.certificate=client.crt' + - 'org.springframework.boot.sslbundle.pem.keystore.private-key=client.key' + - 'org.springframework.boot.sslbundle.pem.truststore.certificate=ca.crt' +secrets: + ssl-ca: + file: 'ca.crt' + ssl-key: + file: 'server.key' + ssl-cert: + file: 'server.crt' diff --git a/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/docker/compose/rabbitmq-ssl.conf b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/docker/compose/rabbitmq-ssl.conf new file mode 100644 index 00000000000..55b842131ca --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/docker/compose/rabbitmq-ssl.conf @@ -0,0 +1,8 @@ +listeners.ssl.default=5671 + +ssl_options.cacertfile=/run/secrets/ssl-ca +ssl_options.certfile=/run/secrets/ssl-cert +ssl_options.keyfile=/run/secrets/ssl-key + +ssl_options.verify=verify_peer +ssl_options.fail_if_no_peer_cert=true diff --git a/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/server.crt b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/server.crt new file mode 100644 index 00000000000..57c66cc78a3 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/server.crt @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEWjCCAkKgAwIBAgIURBZvq442tp+/K9TZII5Vy/LzVxwwDQYJKoZIhvcNAQEL +BQAwOzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MB4XDTI0MDUwMTE2NTMyNVoXDTM0MDQyOTE2NTMyNVow +LzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDESMBAGA1UEAwwJbG9jYWxob3N0 +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsllxsSQzTTJlNHMfXC2b +CIXCPsfCgCBl7FbPz828jwJk+EYcXh0+WTFGks0WxSwb8NQza5UtyCUDEueZj9fV +j5mWBY97WCu01Sl/3xClHmYisXfyyv27GKec7PaSOurCm2JDkyHRNumiJROa4jte +N0GOHzw7FYsM3779TuNw14/gtW+eBrGnvgrpU7fbUvx42Di6ftGYQUwIi+3uIaqT +//i7ktDMaAQJtkL6haTzZ5JN2qKO5a34/WRz/ApvPw3lpDV8c4qoTk3C0Bg9MP+a +DnZtjtLBSN9CJWwr+n11QaMgHTotEKsOahGdi3J2zYxCvJP0LT+hjN2O9aRzSMIs +MwIDAQABo2IwYDALBgNVHQ8EBAMCBaAwEQYJYIZIAYb4QgEBBAQDAgZAMB0GA1Ud +DgQWBBS9XQHGwJZhG0olAGM1UMNuwZ65DzAfBgNVHSMEGDAWgBRVMLDVqPECWaH6 +GruL9E52VcTrPjANBgkqhkiG9w0BAQsFAAOCAgEAhBcqm5UQahn8iFMETXvfLMR6 +OOPijsHQ5lVfhig08s46a9O5eaJ9EYSYyiDnxYvZ4gYVH03f/kPwNLamvGR5KIBQ +R0DltkPPX4a11/vjwlSq1cXAt9r59nY+sNcVXWgIWH7zNodL8lyTpYhqvB2wEQkx +t2/JKZ8A0sGjed4S6I5HofYd7bnBxQZgfZShQ2SdDbzbcyg4SCEb8ghwnsH0KNZo +jJF+20RpK2VMViE6lylLTEMd/PyAdST/NPoqVxyva3QjTrKt+tkkFTsmNVMXcmYC +f1xo1/YFp73FFE63VYFI+Yw+Ajau8sYSo4+YvgFCy+Efhf3h3GFDtaiNod56uX9G +9M/cu8XsFzFP2e/0YWY3XL+v7ESOdc3g7yS4FQZ7Z6YvfAed9hCB25cDECvZXqJG +HSYDR38NHyAPROuCwlEwDyVmWRl9bpwZt+hr9kaTQScIDx+rV/EF3o0GKIwtR7AK +jaPAta0f4/Uu+EuWAcccSRUMtfx5/Jse/6iliBvy7JXmA+Y0PrT7K4uHO7iktdI+ +x8WbfZKfnLVuqw5fneTjC1n48Ltjis/f8DgO7BuWTmLdZXddjqqxzBSukFTBn4Hg +/oSg3XiMywOAVrRCNJehcdTG0u/BqZsrRjcYAJaf5qG/0tMLNsuF9Y53XQQAeezE +etL+7y0mkeQhVF+Kmy4= +-----END CERTIFICATE----- diff --git a/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/server.key b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/server.key new file mode 100644 index 00000000000..95e2ef3e8b3 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEugIBADANBgkqhkiG9w0BAQEFAASCBKQwggSgAgEAAoIBAQCyWXGxJDNNMmU0 +cx9cLZsIhcI+x8KAIGXsVs/PzbyPAmT4RhxeHT5ZMUaSzRbFLBvw1DNrlS3IJQMS +55mP19WPmZYFj3tYK7TVKX/fEKUeZiKxd/LK/bsYp5zs9pI66sKbYkOTIdE26aIl +E5riO143QY4fPDsViwzfvv1O43DXj+C1b54Gsae+CulTt9tS/HjYOLp+0ZhBTAiL +7e4hqpP/+LuS0MxoBAm2QvqFpPNnkk3aoo7lrfj9ZHP8Cm8/DeWkNXxziqhOTcLQ +GD0w/5oOdm2O0sFI30IlbCv6fXVBoyAdOi0Qqw5qEZ2LcnbNjEK8k/QtP6GM3Y71 +pHNIwiwzAgMBAAECgf9REZuCvy2Bi8SoTnjqQuHG5FuA6cPuisuFZr1k88IO+zJQ +uY3WKNs29BV+LcxnoK29W8jQnjqPHXcMfrF5dVWmkrrJdu8JLaGWVHF+uBq8nRb0 +2LvREh5XhZTGzIESNdc/7GIxdouag/8FlzCUYQGuT3v9+wUCiim+4CuIuPvv7ncD +8vANe3Ua5G0mHjVshOiMNpegg45zYlzYpMtUFPs+asLilW6A7UlgC+pLZ1cHUUlU +ZB7KOGT9JdrZpilTidl6LLvDDQK30TSWz8A26SuEAE71DR2VEjLVpjTNS76vlx+c +CrYr/WwpMb0xul+e/uHiNgo+51FiTiJ/IfuGeskCgYEA804CXQM6i5m4/Upps2yG +aTae5xBaYUquZREp5Zb054U6lUAHI41iTMTIwTTvWn5ogNojgi+YjljkzRj2RQ5k +NccBkjBBwwUNVWpBoGeZ73KAdejNB4C4ucGc2kkqEDo4MU5x3IE4JK1Yi1jl9mKb +IR6m3pqb2PCQHjO8sqKNHYkCgYEAu6fH/qUd/XGmCZJWY5K6jg3dISXH16MTO5M+ +jetprkGMMybWKZQa1GedXurPexE48oRlRhkjdQkW6Wcj1Qh6OKp6N2Zx8sY4dLeQ +yVChnMPFE2LK+UlRCKJUZi+rzX415ML6pZg+yW7O2cHpMKv7PlXISw2YDqtboCAi +Y+doqNsCgYBE1yqmBJbZDuqfiCF2KduyA0lcmWzpIEdNw1h2ZIrwwup7dj1O2t8Y +V4lx2TdsBF4vLwli+XKRvCcovMpZaaQC70bLhSnmMxS9uS3OY+HTNTORqQfx+oLJ +1DU8Mf1b0A08LjTbLhijkASAkOuoFehMq66NR3OXIyGz2fGnHYUN+QKBgCC47SL2 +X/hl7PIWVoIef/FtcXXqRKLRiPUGhA3zUwZT38K7rvSpItSPDN4UTAHFywxfEdnb +YFd0Mk6Y8aKgS8+9ynoGnzAaaJXRvKmeKdBQQvlSbNpzcnHy/IylG2xF6dfuOA7Q +MYKmk+Nc8PDPzIveIYMU58MHFn8hm12YaKOpAoGAV1CE8hFkEK9sbRGoKNJkx9nm +CZTv7PybaG/RN4ZrBSwVmnER0FEagA/Tzrlp1pi3sC8ZsC9onSOf6Btq8ZE0zbO1 +vsAm3gTBXcrCJxzw0Wjt8pzEbk3yELm4WE6VDEx4da2jWocdspslpIwdjHnPwsbH +r5O3ZAgigZs/ZtKW/U4= +-----END PRIVATE KEY----- diff --git a/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/testcontainers/rabbitmq-ssl.conf b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/testcontainers/rabbitmq-ssl.conf new file mode 100644 index 00000000000..b954a8b729e --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/rabbitmq/testcontainers/rabbitmq-ssl.conf @@ -0,0 +1,8 @@ +listeners.ssl.default=5671 + +ssl_options.cacertfile=/etc/rabbitmq/ca.crt +ssl_options.certfile=/etc/rabbitmq/server.crt +ssl_options.keyfile=/etc/rabbitmq/server.key + +ssl_options.verify=verify_peer +ssl_options.fail_if_no_peer_cert=true diff --git a/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/spring.properties b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/spring.properties new file mode 100644 index 00000000000..47dff33f0bb --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/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-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpEnvironmentCustomizer.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpEnvironmentCustomizer.java new file mode 100644 index 00000000000..3bc6fdac993 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpEnvironmentCustomizer.java @@ -0,0 +1,40 @@ +/* + * 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.rabbitmq.autoconfigure; + +import com.rabbitmq.client.amqp.Environment; +import com.rabbitmq.client.amqp.impl.AmqpEnvironmentBuilder; + +/** + * Callback interface that can be implemented by beans wishing to customize the + * auto-configured AMQP {@link Environment} that is created by an + * {@link AmqpEnvironmentBuilder}. + * + * @author Eddú Meléndez + * @author Stephane Nicoll + * @since 4.2.0 + */ +@FunctionalInterface +public interface AmqpEnvironmentCustomizer { + + /** + * Customize the {@link AmqpEnvironmentBuilder}. + * @param environmentBuilder the environment builder to customize + */ + void customize(AmqpEnvironmentBuilder environmentBuilder); + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitAnnotationDrivenConfiguration.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitAnnotationDrivenConfiguration.java new file mode 100644 index 00000000000..244c4aa849a --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitAnnotationDrivenConfiguration.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.rabbitmq.autoconfigure; + +import org.springframework.amqp.rabbit.annotation.EnableRabbit; +import org.springframework.amqp.rabbit.config.ContainerCustomizer; +import org.springframework.amqp.rabbit.config.RabbitListenerConfigUtils; +import org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory; +import org.springframework.amqp.rabbitmq.client.config.RabbitAmqpListenerContainerFactory; +import org.springframework.amqp.rabbitmq.client.listener.RabbitAmqpListenerContainer; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.TaskScheduler; + +/** + * Configuration for Spring AMQP annotation-driven endpoints using RabbitMQ. + * + * @author Eddú Meléndez + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(EnableRabbit.class) +class AmqpRabbitAnnotationDrivenConfiguration { + + @Bean + @ConditionalOnMissingBean + RabbitAmqpListenerContainerFactoryConfigurer rabbitAmqpListenerContainerFactoryConfigurer( + AmqpRabbitProperties properties, ObjectProvider messageConverter, + ObjectProvider taskSchedulers) { + RabbitAmqpListenerContainerFactoryConfigurer configurer = new RabbitAmqpListenerContainerFactoryConfigurer( + properties); + messageConverter.ifAvailable(configurer::setMessageConverter); + taskSchedulers.ifAvailable(configurer::setTaskScheduler); + return configurer; + } + + @Bean(name = "rabbitListenerContainerFactory") + @ConditionalOnMissingBean(name = "rabbitListenerContainerFactory") + RabbitAmqpListenerContainerFactory rabbitAmqpListenerContainerFactory( + RabbitAmqpListenerContainerFactoryConfigurer configurer, AmqpConnectionFactory connectionFactory, + ObjectProvider> amqpContainerCustomizer) { + RabbitAmqpListenerContainerFactory factory = new RabbitAmqpListenerContainerFactory(connectionFactory); + configurer.configure(factory); + amqpContainerCustomizer.ifUnique(factory::setContainerCustomizer); + return factory; + } + + @Configuration(proxyBeanMethods = false) + @EnableRabbit + @ConditionalOnMissingBean(name = RabbitListenerConfigUtils.RABBIT_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME) + static class EnableRabbitConfiguration { + + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitAutoConfiguration.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitAutoConfiguration.java new file mode 100644 index 00000000000..a5ad684f756 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitAutoConfiguration.java @@ -0,0 +1,119 @@ +/* + * 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.rabbitmq.autoconfigure; + +import javax.net.ssl.SSLContext; + +import com.rabbitmq.client.amqp.Connection; +import com.rabbitmq.client.amqp.CredentialsProvider; +import com.rabbitmq.client.amqp.Environment; +import com.rabbitmq.client.amqp.impl.AmqpEnvironmentBuilder; +import com.rabbitmq.client.amqp.impl.AmqpEnvironmentBuilder.EnvironmentConnectionSettings; +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpAdmin; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; +import org.springframework.amqp.rabbitmq.client.SingleAmqpConnectionFactory; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitConnectionDetails.Address; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; + +/** + * Auto-configuration for AMQP 1.0 support with RabbitMQ. + * + * @author Eddú Meléndez + * @since 4.2.0 + */ +@AutoConfiguration(after = TaskSchedulingAutoConfiguration.class) +@ConditionalOnClass({ RabbitAmqpTemplate.class, Connection.class }) +@EnableConfigurationProperties(AmqpRabbitProperties.class) +@Import(AmqpRabbitAnnotationDrivenConfiguration.class) +public final class AmqpRabbitAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + AmqpRabbitConnectionDetails amqpRabbitConnectionDetails(AmqpRabbitProperties properties, + @Nullable SslBundles sslBundles) { + return new PropertiesAmqpRabbitConnectionDetails(properties, sslBundles); + } + + @Bean + @ConditionalOnMissingBean + Environment amqpRabbitEnvironment(AmqpRabbitConnectionDetails connectionDetails, + ObjectProvider customizers, + ObjectProvider credentialsProvider) { + PropertyMapper map = PropertyMapper.get(); + EnvironmentConnectionSettings environmentConnectionSettings = new AmqpEnvironmentBuilder().connectionSettings(); + Address address = connectionDetails.getAddress(); + map.from(address::host).to(environmentConnectionSettings::host); + map.from(address::port).to(environmentConnectionSettings::port); + map.from(connectionDetails::getUsername).to(environmentConnectionSettings::username); + map.from(connectionDetails::getPassword).to(environmentConnectionSettings::password); + map.from(connectionDetails::getVirtualHost).to(environmentConnectionSettings::virtualHost); + map.from(credentialsProvider::getIfAvailable).to(environmentConnectionSettings::credentialsProvider); + SslBundle sslBundle = connectionDetails.getSslBundle(); + if (sslBundle != null) { + SSLContext sslContext = sslBundle.createSslContext(); + environmentConnectionSettings.tls().sslContext(sslContext); + } + AmqpEnvironmentBuilder builder = environmentConnectionSettings.environmentBuilder(); + customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); + return builder.build(); + } + + @Bean + @ConditionalOnMissingBean + AmqpConnectionFactory amqpRabbitConnectionFactory(Environment environment) { + return new SingleAmqpConnectionFactory(environment); + } + + @Bean + @ConditionalOnMissingBean + RabbitAmqpTemplate rabbitAmqpTemplate(AmqpConnectionFactory connectionFactory, AmqpRabbitProperties properties, + ObjectProvider customizers, + ObjectProvider messageConverter) { + RabbitAmqpTemplate rabbitAmqpTemplate = new RabbitAmqpTemplate(connectionFactory); + messageConverter.ifAvailable(rabbitAmqpTemplate::setMessageConverter); + AmqpRabbitProperties.Template templateProperties = properties.getTemplate(); + + PropertyMapper map = PropertyMapper.get(); + map.from(templateProperties::getDefaultReceiveQueue).to(rabbitAmqpTemplate::setReceiveQueue); + map.from(templateProperties::getExchange).to(rabbitAmqpTemplate::setExchange); + map.from(templateProperties::getRoutingKey).to(rabbitAmqpTemplate::setRoutingKey); + + customizers.orderedStream().forEach((customizer) -> customizer.customize(rabbitAmqpTemplate)); + return rabbitAmqpTemplate; + } + + @Bean + @ConditionalOnMissingBean + RabbitAmqpAdmin rabbitAmqpAdmin(AmqpConnectionFactory connectionFactory) { + return new RabbitAmqpAdmin(connectionFactory); + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitConnectionDetails.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitConnectionDetails.java new file mode 100644 index 00000000000..364918a6c05 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitConnectionDetails.java @@ -0,0 +1,79 @@ +/* + * 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.rabbitmq.autoconfigure; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; +import org.springframework.boot.ssl.SslBundle; + +/** + * Details required to establish a connection to a RabbitMQ AMQP service. + * + * @author Eddú Meléndez + * @since 4.2.0 + */ +public interface AmqpRabbitConnectionDetails extends ConnectionDetails { + + /** + * Return the 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; + } + + /** + * Virtual host to use when connecting to the broker. + * @return the virtual host to use when connecting to the broker or {@code null} + */ + default @Nullable String getVirtualHost() { + return null; + } + + /** + * SSL bundle to use. + * @return the SSL bundle to use or {@code null} + */ + default @Nullable SslBundle getSslBundle() { + return null; + } + + /** + * A RabbitMQ address. + * + * @param host the host + * @param port the port + */ + record Address(String host, int port) { + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitProperties.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitProperties.java new file mode 100644 index 00000000000..92aa9c12f14 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitProperties.java @@ -0,0 +1,427 @@ +/* + * 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.rabbitmq.autoconfigure; + +import java.time.Duration; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; +import org.springframework.util.StringUtils; + +/** + * Configuration properties for Rabbit AMQP. + * + * @author Eddú Meléndez + * @since 4.2.0 + */ +@ConfigurationProperties("spring.amqp.rabbitmq") +public class AmqpRabbitProperties { + + static final int DEFAULT_PORT = 5672; + + /** + * RabbitMQ host. Ignored if an address is set. + */ + private String host = "localhost"; + + /** + * RabbitMQ port. Ignored if an address is set. Default to 5672, or 5671 if SSL is + * enabled. + */ + private @Nullable Integer port; + + /** + * Login user to authenticate to the broker. + */ + private String username = "guest"; + + /** + * Login to authenticate against the broker. + */ + private String password = "guest"; + + /** + * Virtual host to use when connecting to the broker. + */ + private @Nullable String virtualHost; + + /** + * The address to which the client should connect. When set, the host and port are + * ignored. + */ + private @Nullable String address; + + /** + * SSL configuration. + */ + private final Ssl ssl = new Ssl(); + + /** + * Listener container configuration. + */ + private final Listener listener = new Listener(); + + /** + * Template configuration. + */ + private final Template template = new Template(); + + private @Nullable Address parsedAddress; + + public String getHost() { + return this.host; + } + + public void setHost(String host) { + this.host = host; + } + + public @Nullable Integer getPort() { + return this.port; + } + + /** + * Returns the port from the address, or the configured port if no address have been + * set. + * @return the port + * @see #setAddress(String) + * @see #getPort() + */ + public int determinePort() { + if (this.parsedAddress == null) { + Integer port = getPort(); + if (port != null) { + return port; + } + return DEFAULT_PORT; + } + return this.parsedAddress.port; + } + + public void setPort(@Nullable Integer port) { + this.port = port; + } + + public @Nullable String getAddress() { + return this.address; + } + + /** + * Returns the configured address ({@code host:port}) created from the configured host + * and port. + * @return the address + */ + public String determineAddress() { + if (this.parsedAddress == null) { + if (this.host.contains(",")) { + throw new InvalidConfigurationPropertyValueException("spring.amqp.rabbitmq.host", this.host, + "Invalid character ','. Value must be a single host. For multiple hosts, use property 'spring.amqp.address' instead."); + } + return this.host + ":" + determinePort(); + } + return this.parsedAddress.host + ":" + this.parsedAddress.port; + } + + public void setAddress(String address) { + this.address = address; + this.parsedAddress = parseAddress(address); + } + + private Address parseAddress(String address) { + return new Address(address); + } + + public String getUsername() { + return this.username; + } + + /** + * If address has been set and has a username it is returned. Otherwise returns the + * result of calling {@code getUsername()}. + * @return the username + * @see #setAddress(String) + * @see #getUsername() + */ + public String determineUsername() { + if (this.parsedAddress == null) { + return this.username; + } + Address address = this.parsedAddress; + return (address.username != null) ? address.username : this.username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return this.password; + } + + /** + * If address has been set and has a password it is returned. Otherwise returns the + * result of calling {@code getPassword()}. + * @return the password or {@code null} + * @see #setAddress(String) + * @see #getPassword() + */ + public @Nullable String determinePassword() { + if (this.parsedAddress == null) { + return getPassword(); + } + Address address = this.parsedAddress; + return (address.password != null) ? address.password : getPassword(); + } + + public void setPassword(String password) { + this.password = password; + } + + public @Nullable String getVirtualHost() { + return this.virtualHost; + } + + /** + * If address has been set and has a virtual host it is returned. Otherwise returns + * the result of calling {@code getVirtualHost()}. + * @return the virtual host or {@code null} + * @see #setAddress(String) + * @see #getVirtualHost() + */ + public @Nullable String determineVirtualHost() { + if (this.parsedAddress == null) { + return getVirtualHost(); + } + Address address = this.parsedAddress; + return (address.virtualHost != null) ? address.virtualHost : getVirtualHost(); + } + + public void setVirtualHost(@Nullable String virtualHost) { + this.virtualHost = StringUtils.hasText(virtualHost) ? virtualHost : "/"; + } + + public Ssl getSsl() { + return this.ssl; + } + + public Listener getListener() { + return this.listener; + } + + public Template getTemplate() { + return this.template; + } + + /** + * SSL configuration. + */ + public static class Ssl { + + /** + * SSL bundle name. + */ + private @Nullable String bundle; + + public @Nullable String getBundle() { + return this.bundle; + } + + public void setBundle(@Nullable String bundle) { + this.bundle = bundle; + } + + } + + public static class Listener { + + /** + * Whether to enable observation. + */ + private boolean observationEnabled; + + private final Batch batch = new Batch(); + + public boolean isObservationEnabled() { + return this.observationEnabled; + } + + public void setObservationEnabled(boolean observationEnabled) { + this.observationEnabled = observationEnabled; + } + + public Batch getBatch() { + return this.batch; + } + + public static class Batch { + + /** + * Batch size, expressed as the number of physical messages, to be used by the + * container. + */ + private @Nullable Integer size; + + /** + * Timeout for gathering batch messages. + */ + private Duration receiveTimeout = Duration.ofSeconds(30); + + public @Nullable Integer getSize() { + return this.size; + } + + public void setSize(@Nullable Integer size) { + this.size = size; + } + + public Duration getReceiveTimeout() { + return this.receiveTimeout; + } + + public void setReceiveTimeout(Duration receiveTimeout) { + this.receiveTimeout = receiveTimeout; + } + + } + + } + + public static class Template { + + /** + * Name of the default exchange to use for send operations. + */ + private String exchange = ""; + + /** + * Value of a default routing key to use for send operations. + */ + private String routingKey = ""; + + /** + * Name of the default queue to receive messages from when none is specified + * explicitly. + */ + private @Nullable String defaultReceiveQueue; + + public String getExchange() { + return this.exchange; + } + + public void setExchange(String exchange) { + this.exchange = exchange; + } + + public String getRoutingKey() { + return this.routingKey; + } + + public void setRoutingKey(String routingKey) { + this.routingKey = routingKey; + } + + public @Nullable String getDefaultReceiveQueue() { + return this.defaultReceiveQueue; + } + + public void setDefaultReceiveQueue(@Nullable String defaultReceiveQueue) { + this.defaultReceiveQueue = defaultReceiveQueue; + } + + } + + private static final class Address { + + private static final String PREFIX_AMQP = "amqp://"; + + private static final String PREFIX_AMQP_SECURE = "amqps://"; + + private String host; + + private int port; + + private @Nullable String username; + + private @Nullable String password; + + private @Nullable String virtualHost; + + private Address(String input) { + input = input.trim(); + input = trimPrefix(input); + input = parseUsernameAndPassword(input); + input = parseVirtualHost(input); + parseHostAndPort(input); + } + + private String trimPrefix(String input) { + if (input.startsWith(PREFIX_AMQP_SECURE)) { + return input.substring(PREFIX_AMQP_SECURE.length()); + } + if (input.startsWith(PREFIX_AMQP)) { + return input.substring(PREFIX_AMQP.length()); + } + return input; + } + + private String parseUsernameAndPassword(String input) { + String[] splitInput = StringUtils.split(input, "@"); + if (splitInput == null) { + return input; + } + String credentials = splitInput[0]; + String[] splitCredentials = StringUtils.split(credentials, ":"); + if (splitCredentials == null) { + this.username = credentials; + } + else { + this.username = splitCredentials[0]; + this.password = splitCredentials[1]; + } + return splitInput[1]; + } + + private String parseVirtualHost(String input) { + int hostIndex = input.indexOf('/'); + if (hostIndex >= 0) { + this.virtualHost = input.substring(hostIndex + 1); + if (this.virtualHost.isEmpty()) { + this.virtualHost = "/"; + } + input = input.substring(0, hostIndex); + } + return input; + } + + private void parseHostAndPort(String input) { + int bracketIndex = input.lastIndexOf(']'); + int colonIndex = input.lastIndexOf(':'); + if (colonIndex == -1 || colonIndex < bracketIndex) { + this.host = input; + this.port = DEFAULT_PORT; + } + else { + this.host = input.substring(0, colonIndex); + this.port = Integer.parseInt(input.substring(colonIndex + 1)); + } + } + + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/PropertiesAmqpRabbitConnectionDetails.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/PropertiesAmqpRabbitConnectionDetails.java new file mode 100644 index 00000000000..9f1d9b71ed3 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/PropertiesAmqpRabbitConnectionDetails.java @@ -0,0 +1,76 @@ +/* + * 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.rabbitmq.autoconfigure; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Adapts {@link AmqpRabbitProperties} to {@link AmqpRabbitConnectionDetails}. + * + * @author Eddú Meléndez + */ +class PropertiesAmqpRabbitConnectionDetails implements AmqpRabbitConnectionDetails { + + private final AmqpRabbitProperties properties; + + private final @Nullable SslBundles sslBundles; + + PropertiesAmqpRabbitConnectionDetails(AmqpRabbitProperties properties, @Nullable SslBundles sslBundles) { + this.properties = properties; + this.sslBundles = sslBundles; + } + + @Override + public Address getAddress() { + String address = this.properties.determineAddress(); + int portSeparatorIndex = address.lastIndexOf(':'); + String host = address.substring(0, portSeparatorIndex); + String port = address.substring(portSeparatorIndex + 1); + return new Address(host, Integer.parseInt(port)); + } + + @Override + public String getUsername() { + return this.properties.determineUsername(); + } + + @Override + public @Nullable String getPassword() { + return this.properties.determinePassword(); + } + + @Override + public @Nullable String getVirtualHost() { + return this.properties.determineVirtualHost(); + } + + @Override + public @Nullable SslBundle getSslBundle() { + String bundle = this.properties.getSsl().getBundle(); + if (StringUtils.hasLength(bundle)) { + Assert.notNull(this.sslBundles, "SSL bundle name has been set but no SSL bundles found in context"); + return this.sslBundles.getBundle(bundle); + } + return null; + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/RabbitAmqpListenerContainerFactoryConfigurer.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/RabbitAmqpListenerContainerFactoryConfigurer.java new file mode 100644 index 00000000000..deae4330a18 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/RabbitAmqpListenerContainerFactoryConfigurer.java @@ -0,0 +1,92 @@ +/* + * 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.rabbitmq.autoconfigure; + +import java.time.Duration; + +import org.jspecify.annotations.Nullable; + +import org.springframework.amqp.rabbitmq.client.config.RabbitAmqpListenerContainerFactory; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitProperties.Listener; +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitProperties.Listener.Batch; +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.util.Assert; + +/** + * Configure {@link RabbitAmqpListenerContainerFactory} with sensible defaults tuned using + * configuration properties. + *

+ * Can be injected into application code and used to define a custom + * {@code RabbitAmqpListenerContainerFactory} whose configuration is based upon that + * produced by auto-configuration. + * + * @author Stephane Nicoll + * @since 4.2.0 + */ +public final class RabbitAmqpListenerContainerFactoryConfigurer { + + private final AmqpRabbitProperties properties; + + private @Nullable MessageConverter messageConverter; + + private @Nullable TaskScheduler taskScheduler; + + /** + * Creates a new configurer that will use the given {@code properties}. + * @param properties the properties to use + */ + public RabbitAmqpListenerContainerFactoryConfigurer(AmqpRabbitProperties properties) { + this.properties = properties; + } + + /** + * Set the {@link MessageConverter} to use or {@code null} if the out-of-the-box + * converter should be used. + * @param messageConverter the {@link MessageConverter} + */ + protected void setMessageConverter(@Nullable MessageConverter messageConverter) { + this.messageConverter = messageConverter; + } + + /** + * Set the {@link TaskScheduler} to use or {@code null} if the default should be used. + * @param taskScheduler the {@link TaskScheduler} + */ + protected void setTaskScheduler(@Nullable TaskScheduler taskScheduler) { + this.taskScheduler = taskScheduler; + } + + /** + * Configure the specified {@link RabbitAmqpListenerContainerFactory}. The factory can + * be further tuned and default settings can be overridden. + * @param factory the {@link RabbitAmqpListenerContainerFactory} instance to configure + */ + public void configure(RabbitAmqpListenerContainerFactory factory) { + Assert.notNull(factory, "'factory' must not be null"); + PropertyMapper map = PropertyMapper.get(); + map.from(this.messageConverter).to(factory::setMessageConverter); + map.from(this.taskScheduler).to(factory::setTaskScheduler); + Listener listenerProperties = this.properties.getListener(); + map.from(listenerProperties.isObservationEnabled()).to(factory::setObservationEnabled); + Batch batchProperties = listenerProperties.getBatch(); + map.from(batchProperties.getSize()).to(factory::setBatchSize); + map.from(batchProperties.getReceiveTimeout()).as(Duration::toMillis).to(factory::setBatchReceiveTimeout); + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/RabbitAmqpTemplateCustomizer.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/RabbitAmqpTemplateCustomizer.java new file mode 100644 index 00000000000..f47c3a02843 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/RabbitAmqpTemplateCustomizer.java @@ -0,0 +1,36 @@ +/* + * 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.rabbitmq.autoconfigure; + +import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; + +/** + * Callback interface that can be used to customize a {@link RabbitAmqpTemplate}. + * + * @author Eddú Meléndez + * @since 4.2.0 + */ +@FunctionalInterface +public interface RabbitAmqpTemplateCustomizer { + + /** + * Callback to customize a {@link RabbitAmqpTemplate} instance. + * @param rabbitAmqpTemplate the rabbitAmqpTemplate to customize + */ + void customize(RabbitAmqpTemplate rabbitAmqpTemplate); + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/health/AmqpRabbitHealthContributorAutoConfiguration.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/health/AmqpRabbitHealthContributorAutoConfiguration.java new file mode 100644 index 00000000000..d4d9b367768 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/health/AmqpRabbitHealthContributorAutoConfiguration.java @@ -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. + */ + +package org.springframework.boot.amqp.rabbitmq.autoconfigure.health; + +import com.rabbitmq.client.amqp.Connection; + +import org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitAutoConfiguration; +import org.springframework.boot.amqp.rabbitmq.health.AmqpRabbitHealthIndicator; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +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.health.autoconfigure.contributor.CompositeHealthContributorConfiguration; +import org.springframework.boot.health.autoconfigure.contributor.ConditionalOnEnabledHealthIndicator; +import org.springframework.boot.health.contributor.HealthContributor; +import org.springframework.context.annotation.Bean; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for + * {@link AmqpRabbitHealthIndicator}. + * + * @author Stephane Nicoll + * @since 4.2.0 + */ +@AutoConfiguration(after = AmqpRabbitAutoConfiguration.class) +@ConditionalOnClass({ AmqpRabbitHealthIndicator.class, RabbitAmqpTemplate.class, Connection.class, + ConditionalOnEnabledHealthIndicator.class }) +@ConditionalOnBean(AmqpConnectionFactory.class) +@ConditionalOnEnabledHealthIndicator("rabbit") +public final class AmqpRabbitHealthContributorAutoConfiguration + extends CompositeHealthContributorConfiguration { + + AmqpRabbitHealthContributorAutoConfiguration() { + super(AmqpRabbitHealthIndicator::new); + } + + @Bean + @ConditionalOnMissingBean(name = { "rabbitHealthIndicator", "rabbitHealthContributor" }) + HealthContributor rabbitHealthContributor(ConfigurableListableBeanFactory beanFactory) { + return createContributor(beanFactory, AmqpConnectionFactory.class); + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/health/package-info.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/health/package-info.java new file mode 100644 index 00000000000..dfd3495d0d2 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/health/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 AMQP 1.0 support with RabbitMQ. + */ +@NullMarked +package org.springframework.boot.amqp.rabbitmq.autoconfigure.health; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/package-info.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/package-info.java new file mode 100644 index 00000000000..e2b848d419a --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/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 AMQP 1.0 support with RabbitMQ. + */ +@NullMarked +package org.springframework.boot.amqp.rabbitmq.autoconfigure; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/docker/compose/AmqpRabbitMqDockerComposeConnectionDetailsFactory.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/docker/compose/AmqpRabbitMqDockerComposeConnectionDetailsFactory.java new file mode 100644 index 00000000000..4c2a4d16e83 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/docker/compose/AmqpRabbitMqDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,103 @@ +/* + * 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.rabbitmq.docker.compose; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitConnectionDetails; +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; +import org.springframework.boot.ssl.SslBundle; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create + * {@link AmqpRabbitConnectionDetails} for a {@code rabbitmq} service. + * + * @author Eddú Meléndez + */ +class AmqpRabbitMqDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { + + private static final int RABBITMQ_PORT = 5672; + + private static final int RABBITMQ_TLS_PORT = 5671; + + protected AmqpRabbitMqDockerComposeConnectionDetailsFactory() { + super("rabbitmq"); + } + + @Override + protected @Nullable AmqpRabbitConnectionDetails getDockerComposeConnectionDetails( + DockerComposeConnectionSource source) { + try { + return new AmqpRabbitMqDockerComposeRabbitConnectionDetails(source.getRunningService()); + } + catch (IllegalStateException ex) { + return null; + } + } + + /** + * {@link AmqpRabbitConnectionDetails} backed by a {@code rabbitmq} + * {@link RunningService}. + */ + static class AmqpRabbitMqDockerComposeRabbitConnectionDetails extends DockerComposeConnectionDetails + implements AmqpRabbitConnectionDetails { + + private final Address address; + + private final RabbitEnvironment environment; + + private final @Nullable SslBundle sslBundle; + + protected AmqpRabbitMqDockerComposeRabbitConnectionDetails(RunningService service) { + super(service); + this.sslBundle = getSslBundle(service); + int containerPort = (this.sslBundle != null) ? RABBITMQ_TLS_PORT : RABBITMQ_PORT; + this.address = new Address(service.host(), service.ports().get(containerPort)); + this.environment = new RabbitEnvironment(service.env()); + } + + @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(); + } + + @Override + public String getVirtualHost() { + return "/"; + } + + @Override + public @Nullable SslBundle getSslBundle() { + return this.sslBundle; + } + + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/docker/compose/RabbitEnvironment.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/docker/compose/RabbitEnvironment.java new file mode 100644 index 00000000000..709ccb58991 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/docker/compose/RabbitEnvironment.java @@ -0,0 +1,47 @@ +/* + * 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.rabbitmq.docker.compose; + +import java.util.Map; + +import org.jspecify.annotations.Nullable; + +/** + * RabbitMQ environment details. + * + * @author Eddú Meléndez + */ +class RabbitEnvironment { + + private final @Nullable String username; + + private final @Nullable String password; + + RabbitEnvironment(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-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/docker/compose/package-info.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/docker/compose/package-info.java new file mode 100644 index 00000000000..94e20efb537 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/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 RabbitMQ service connections. + */ +@NullMarked +package org.springframework.boot.amqp.rabbitmq.docker.compose; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/health/AmqpRabbitHealthIndicator.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/health/AmqpRabbitHealthIndicator.java new file mode 100644 index 00000000000..5e8d7b0b8a0 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/health/AmqpRabbitHealthIndicator.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.rabbitmq.health; + +import org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory; +import org.springframework.boot.health.contributor.AbstractHealthIndicator; +import org.springframework.boot.health.contributor.Health; +import org.springframework.boot.health.contributor.HealthIndicator; +import org.springframework.util.Assert; + +/** + * {@link HealthIndicator} for AMQP 1.0 support with RabbitMQ. + * + * @author Stephane Nicoll + * @since 4.2.0 + */ +public class AmqpRabbitHealthIndicator extends AbstractHealthIndicator { + + private final AmqpConnectionFactory amqpConnectionFactory; + + public AmqpRabbitHealthIndicator(AmqpConnectionFactory amqpConnectionFactory) { + super("Rabbit health check failed"); + Assert.notNull(amqpConnectionFactory, "'amqpConnectionFactory' must not be null"); + this.amqpConnectionFactory = amqpConnectionFactory; + } + + @Override + protected void doHealthCheck(Health.Builder builder) throws Exception { + builder.up(); + String version = this.amqpConnectionFactory.getConnection().connectionInfo().brokerVersion(); + if (version != null) { + builder.withDetail("version", version); + } + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/health/package-info.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/health/package-info.java new file mode 100644 index 00000000000..4883951f350 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/health/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. + */ + +/** + * Health integration for AMQP 1.0 support with RabbitMQ. + */ +@NullMarked +package org.springframework.boot.amqp.rabbitmq.health; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/testcontainers/AmqpRabbitMqContainerConnectionDetailsFactory.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/testcontainers/AmqpRabbitMqContainerConnectionDetailsFactory.java new file mode 100644 index 00000000000..717049be83e --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/testcontainers/AmqpRabbitMqContainerConnectionDetailsFactory.java @@ -0,0 +1,79 @@ +/* + * 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.rabbitmq.testcontainers; + +import java.net.URI; + +import org.jspecify.annotations.Nullable; +import org.testcontainers.rabbitmq.RabbitMQContainer; + +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitConnectionDetails; +import org.springframework.boot.ssl.SslBundle; +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 AmqpRabbitConnectionDetails} + * from a {@link ServiceConnection @ServiceConnection}-annotated + * {@link RabbitMQContainer}. + * + * @author Eddú Meléndez + */ +class AmqpRabbitMqContainerConnectionDetailsFactory + extends ContainerConnectionDetailsFactory { + + @Override + protected AmqpRabbitConnectionDetails getContainerConnectionDetails( + ContainerConnectionSource source) { + return new AmqpRabbitMqContainerRabbitConnectionDetails(source); + } + + /** + * {@link AmqpRabbitConnectionDetails} backed by a {@link ContainerConnectionSource}. + */ + static final class AmqpRabbitMqContainerRabbitConnectionDetails + extends ContainerConnectionDetails implements AmqpRabbitConnectionDetails { + + private AmqpRabbitMqContainerRabbitConnectionDetails(ContainerConnectionSource source) { + super(source); + } + + @Override + public Address getAddress() { + URI uri = URI.create((getSslBundle() != null) ? getContainer().getAmqpsUrl() : getContainer().getAmqpUrl()); + return new Address(uri.getHost(), uri.getPort()); + } + + @Override + public String getUsername() { + return getContainer().getAdminUsername(); + } + + @Override + public String getPassword() { + return getContainer().getAdminPassword(); + } + + @Override + public @Nullable SslBundle getSslBundle() { + return super.getSslBundle(); + } + + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/testcontainers/package-info.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/testcontainers/package-info.java new file mode 100644 index 00000000000..dc19f3dd342 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/rabbitmq/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 RabbitMQ service connections. + */ +@NullMarked +package org.springframework.boot.amqp.rabbitmq.testcontainers; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp-rabbitmq/src/main/resources/META-INF/spring.factories b/module/spring-boot-amqp-rabbitmq/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000000..b6129e3103c --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +# Connection Details Factories +org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory=\ +org.springframework.boot.amqp.rabbitmq.docker.compose.AmqpRabbitMqDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.amqp.rabbitmq.testcontainers.AmqpRabbitMqContainerConnectionDetailsFactory diff --git a/module/spring-boot-amqp-rabbitmq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/module/spring-boot-amqp-rabbitmq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000000..aa0a1c854fe --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitAutoConfiguration +org.springframework.boot.amqp.rabbitmq.autoconfigure.health.AmqpRabbitHealthContributorAutoConfiguration diff --git a/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitAutoConfigurationTests.java b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitAutoConfigurationTests.java new file mode 100644 index 00000000000..f485de17b05 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitAutoConfigurationTests.java @@ -0,0 +1,294 @@ +/* + * 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.rabbitmq.autoconfigure; + +import com.rabbitmq.client.amqp.Environment; +import com.rabbitmq.client.amqp.impl.AmqpEnvironmentBuilder; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.config.ContainerCustomizer; +import org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpAdmin; +import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; +import org.springframework.amqp.rabbitmq.client.config.RabbitAmqpListenerContainerFactory; +import org.springframework.amqp.rabbitmq.client.listener.RabbitAmqpListenerContainer; +import org.springframework.amqp.support.converter.MessageConversionException; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitConnectionDetails.Address; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; +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.BDDMockito.then; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AmqpRabbitAutoConfiguration}. + * + * @author Eddú Meléndez + */ +class AmqpRabbitAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AmqpRabbitAutoConfiguration.class)); + + @Test + void testDefaultRabbitAmqpConfiguration() { + this.contextRunner.run((context) -> { + RabbitAmqpListenerContainerFactory listenerContainerFactory = context + .getBean(RabbitAmqpListenerContainerFactory.class); + AmqpConnectionFactory connectionFactory = context.getBean(AmqpConnectionFactory.class); + RabbitAmqpTemplate rabbitAmqpTemplate = context.getBean(RabbitAmqpTemplate.class); + RabbitAmqpAdmin rabbitAmqpAdmin = context.getBean(RabbitAmqpAdmin.class); + + assertThat(listenerContainerFactory).isNotNull(); + assertThat(listenerContainerFactory).extracting("containerCustomizer").isNull(); + assertThat(connectionFactory).isNotNull(); + assertThat(rabbitAmqpTemplate).isNotNull(); + assertThat(rabbitAmqpAdmin).isNotNull(); + }); + } + + @Test + void whenMultipleRabbitAmqpTemplateCustomizersAreDefinedThenTheyAreCalledInOrder() { + this.contextRunner.withUserConfiguration(MultipleRabbitAmqpTemplateCustomizersConfiguration.class) + .run((context) -> { + RabbitAmqpTemplateCustomizer firstCustomizer = context.getBean("firstCustomizer", + RabbitAmqpTemplateCustomizer.class); + RabbitAmqpTemplateCustomizer secondCustomizer = context.getBean("secondCustomizer", + RabbitAmqpTemplateCustomizer.class); + InOrder inOrder = inOrder(firstCustomizer, secondCustomizer); + RabbitAmqpTemplate template = context.getBean(RabbitAmqpTemplate.class); + then(firstCustomizer).should(inOrder).customize(template); + then(secondCustomizer).should(inOrder).customize(template); + inOrder.verifyNoMoreInteractions(); + }); + } + + @Test + void testListenerContainerFactoryWithContainerCustomizer() { + this.contextRunner.withUserConfiguration(AmqpContainerCustomizerConfiguration.class).run((context) -> { + RabbitAmqpListenerContainerFactory listenerContainerFactory = context + .getBean(RabbitAmqpListenerContainerFactory.class); + + assertThat(listenerContainerFactory).isNotNull(); + assertThat(listenerContainerFactory).extracting("containerCustomizer").isNotNull(); + }); + } + + @Test + void autoConfigurationConfiguresConnectionDetailsWithDefaultValues() { + this.contextRunner.run((context) -> { + AmqpRabbitConnectionDetails connectionDetails = context.getBean(AmqpRabbitConnectionDetails.class); + AmqpRabbitProperties properties = new AmqpRabbitProperties(); + assertThat(connectionDetails.getAddress().host()).isEqualTo(properties.getHost()); + assertThat(connectionDetails.getAddress().port()).isEqualTo(AmqpRabbitProperties.DEFAULT_PORT); + assertThat(connectionDetails.getUsername()).isEqualTo(properties.getUsername()); + assertThat(connectionDetails.getPassword()).isEqualTo(properties.getPassword()); + assertThat(connectionDetails.getVirtualHost()).isNull(); + }); + } + + @Test + void autoConfigurationConfiguresConnectionDetailsWithPropertyOverrides() { + this.contextRunner + .withPropertyValues("spring.amqp.rabbitmq.host=custom.host", "spring.amqp.rabbitmq.port=1234", + "spring.amqp.rabbitmq.username=user", "spring.amqp.rabbitmq.password=secret") + .run((context) -> { + AmqpRabbitConnectionDetails connectionDetails = context.getBean(AmqpRabbitConnectionDetails.class); + assertThat(connectionDetails.getAddress().host()).isEqualTo("custom.host"); + assertThat(connectionDetails.getAddress().port()).isEqualTo(1234); + assertThat(connectionDetails.getUsername()).isEqualTo("user"); + assertThat(connectionDetails.getPassword()).isEqualTo("secret"); + }); + } + + @Test + void autoConfigurationBacksOffWhenUserProvidesConnectionDetails() { + AmqpRabbitConnectionDetails connectionDetails = mock(AmqpRabbitConnectionDetails.class); + given(connectionDetails.getAddress()).willReturn(new Address("localhost", 5672)); + this.contextRunner.withBean(AmqpRabbitConnectionDetails.class, () -> connectionDetails).run((context) -> { + assertThat(context).hasSingleBean(AmqpRabbitConnectionDetails.class); + assertThat(context.getBean(AmqpRabbitConnectionDetails.class)).isSameAs(connectionDetails); + }); + } + + @Test + void autoConfigurationBacksOffWhenUserProvidesEnvironment() { + Environment environment = mock(Environment.class); + this.contextRunner.withBean(Environment.class, () -> environment).run((context) -> { + assertThat(context).hasSingleBean(Environment.class); + assertThat(context.getBean(Environment.class)).isSameAs(environment); + }); + } + + @Test + void autoConfigurationBacksOffWhenUserProvidesConnectionFactory() { + AmqpConnectionFactory connectionFactory = mock(AmqpConnectionFactory.class); + this.contextRunner.withBean(AmqpConnectionFactory.class, () -> connectionFactory).run((context) -> { + assertThat(context).hasSingleBean(AmqpConnectionFactory.class); + assertThat(context.getBean(AmqpConnectionFactory.class)).isSameAs(connectionFactory); + }); + } + + @Test + void autoConfigurationBacksOffWhenUserProvidesRabbitAmqpTemplate() { + RabbitAmqpTemplate template = mock(RabbitAmqpTemplate.class); + this.contextRunner.withBean(RabbitAmqpTemplate.class, () -> template).run((context) -> { + assertThat(context).hasSingleBean(RabbitAmqpTemplate.class); + assertThat(context.getBean(RabbitAmqpTemplate.class)).isSameAs(template); + }); + } + + @Test + void autoConfigurationBacksOffWhenUserProvidesRabbitAmqpAdmin() { + RabbitAmqpAdmin admin = mock(RabbitAmqpAdmin.class); + this.contextRunner.withBean(RabbitAmqpAdmin.class, () -> admin).run((context) -> { + assertThat(context).hasSingleBean(RabbitAmqpAdmin.class); + assertThat(context.getBean(RabbitAmqpAdmin.class)).isSameAs(admin); + }); + } + + @Test + void autoConfigurationBacksOffWhenUserProvidesRabbitListenerContainerFactory() { + RabbitAmqpListenerContainerFactory factory = mock(RabbitAmqpListenerContainerFactory.class); + this.contextRunner + .withBean("rabbitListenerContainerFactory", RabbitAmqpListenerContainerFactory.class, () -> factory) + .run((context) -> { + assertThat(context).hasSingleBean(RabbitAmqpListenerContainerFactory.class); + assertThat(context.getBean(RabbitAmqpListenerContainerFactory.class)).isSameAs(factory); + }); + } + + @Test + void autoConfigurationAppliesEnvironmentCustomizer() { + AmqpEnvironmentCustomizer customizer = mock(AmqpEnvironmentCustomizer.class); + this.contextRunner.withBean(AmqpEnvironmentCustomizer.class, () -> customizer) + .run((context) -> then(customizer).should().customize(any(AmqpEnvironmentBuilder.class))); + } + + @Test + void autoConfigurationConfiguresConnectionDetailsWithSslBundle() { + SslBundle sslBundle = mock(SslBundle.class); + SslBundles sslBundles = mock(SslBundles.class); + given(sslBundles.getBundle("test-bundle")).willReturn(sslBundle); + this.contextRunner.withPropertyValues("spring.amqp.rabbitmq.ssl.bundle=test-bundle") + .withBean(SslBundles.class, () -> sslBundles) + .run((context) -> { + AmqpRabbitConnectionDetails connectionDetails = context.getBean(AmqpRabbitConnectionDetails.class); + assertThat(connectionDetails.getSslBundle()).isSameAs(sslBundle); + }); + } + + @Test + void autoConfigurationConfiguresRabbitAmqpTemplateWithMessageConverter() { + this.contextRunner.withUserConfiguration(CustomMessageConverterConfiguration.class).run((context) -> { + MessageConverter expected = context.getBean(MessageConverter.class); + RabbitAmqpTemplate template = context.getBean(RabbitAmqpTemplate.class); + assertThat(template).hasFieldOrPropertyWithValue("messageConverter", expected); + }); + } + + @Test + void autoConfigurationConfiguresRabbitAmqpTemplateWithTemplateProperties() { + this.contextRunner + .withPropertyValues("spring.amqp.rabbitmq.template.exchange=my-exchange", + "spring.amqp.rabbitmq.template.routing-key=my-routing-key", + "spring.amqp.rabbitmq.template.default-receive-queue=my-queue") + .run((context) -> { + RabbitAmqpTemplate template = context.getBean(RabbitAmqpTemplate.class); + assertThat(template).hasFieldOrPropertyWithValue("defaultExchange", "my-exchange"); + assertThat(template).hasFieldOrPropertyWithValue("defaultRoutingKey", "my-routing-key"); + assertThat(template).hasFieldOrPropertyWithValue("defaultReceiveQueue", "my-queue"); + }); + } + + @Configuration(proxyBeanMethods = false) + static class MultipleRabbitAmqpTemplateCustomizersConfiguration { + + @Bean + @Order(Ordered.LOWEST_PRECEDENCE) + RabbitAmqpTemplateCustomizer secondCustomizer() { + return mock(RabbitAmqpTemplateCustomizer.class); + } + + @Bean + @Order(0) + RabbitAmqpTemplateCustomizer firstCustomizer() { + return mock(RabbitAmqpTemplateCustomizer.class); + } + + } + + @Import(TestListener.class) + @Configuration(proxyBeanMethods = false) + static class AmqpContainerCustomizerConfiguration { + + @Bean + @SuppressWarnings("unchecked") + ContainerCustomizer customizer() { + return mock(ContainerCustomizer.class); + } + + } + + @Configuration + static class CustomMessageConverterConfiguration { + + @Bean + MessageConverter messageConverter() { + return new MessageConverter() { + + @Override + public Message toMessage(Object object, MessageProperties messageProperties) + throws MessageConversionException { + return new Message(object.toString().getBytes()); + } + + @Override + public Object fromMessage(Message message) throws MessageConversionException { + return new String(message.getBody()); + } + + }; + } + + } + + static class TestListener { + + @RabbitListener(queues = "test", autoStartup = "false") + void listen(String in) { + } + + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitPropertiesTests.java b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitPropertiesTests.java new file mode 100644 index 00000000000..1ca06584808 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/AmqpRabbitPropertiesTests.java @@ -0,0 +1,213 @@ +/* + * 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.rabbitmq.autoconfigure; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link AmqpRabbitProperties}. + * + * @author Stephane Nicoll + */ +class AmqpRabbitPropertiesTests { + + private final AmqpRabbitProperties properties = new AmqpRabbitProperties(); + + @Test + void customHost() { + this.properties.setHost("rabbit.example.com"); + assertThat(this.properties.getHost()).isEqualTo("rabbit.example.com"); + } + + @Test + void customPort() { + this.properties.setPort(1234); + assertThat(this.properties.getPort()).isEqualTo(1234); + } + + @Test + void determinePortDefaultsTo5672() { + assertThat(this.properties.determinePort()).isEqualTo(5672); + } + + @Test + void determinePortReturnsPortFromAddress() { + this.properties.setAddress("rabbit.example.com:1234"); + assertThat(this.properties.determinePort()).isEqualTo(1234); + } + + @Test + void determinePortDefaultsWhenAddressHasNoPort() { + this.properties.setAddress("rabbit.example.com"); + assertThat(this.properties.determinePort()).isEqualTo(5672); + } + + @Test + void customVirtualHost() { + this.properties.setVirtualHost("alpha"); + assertThat(this.properties.getVirtualHost()).isEqualTo("alpha"); + } + + @Test + void virtualHostRetainsALeadingSlash() { + this.properties.setVirtualHost("/alpha"); + assertThat(this.properties.getVirtualHost()).isEqualTo("/alpha"); + } + + @Test + void emptyVirtualHostIsCoercedToASlash() { + this.properties.setVirtualHost(""); + assertThat(this.properties.getVirtualHost()).isEqualTo("/"); + } + + @Test + void customUsername() { + this.properties.setUsername("user"); + assertThat(this.properties.getUsername()).isEqualTo("user"); + } + + @Test + void customPassword() { + this.properties.setPassword("secret"); + assertThat(this.properties.getPassword()).isEqualTo("secret"); + } + + @Test + void customAddress() { + this.properties.setAddress("user:secret@rabbit.example.com:1234/alpha"); + assertThat(this.properties.getAddress()).isEqualTo("user:secret@rabbit.example.com:1234/alpha"); + } + + @Test + void determineAddressUsesHostAndPortDefaults() { + assertThat(this.properties.determineAddress()).isEqualTo("localhost:5672"); + } + + @Test + void determineAddressUsesHostAndPortProperties() { + this.properties.setHost("rabbit.example.com"); + this.properties.setPort(1234); + assertThat(this.properties.determineAddress()).isEqualTo("rabbit.example.com:1234"); + } + + @Test + void determineAddressUsesAddressProperty() { + this.properties.setAddress("rabbit.example.com:1234"); + assertThat(this.properties.determineAddress()).isEqualTo("rabbit.example.com:1234"); + } + + @Test + void determineAddressFromAmqpUrl() { + this.properties.setAddress("amqp://user:secret@rabbit.example.com:1234/alpha"); + assertThat(this.properties.determineAddress()).isEqualTo("rabbit.example.com:1234"); + assertThat(this.properties.determineUsername()).isEqualTo("user"); + assertThat(this.properties.determinePassword()).isEqualTo("secret"); + assertThat(this.properties.determineVirtualHost()).isEqualTo("alpha"); + } + + @Test + void determineAddressFromAmqpsUrl() { + this.properties.setAddress("amqps://user:secret@rabbit.example.com:1234"); + assertThat(this.properties.determineAddress()).isEqualTo("rabbit.example.com:1234"); + } + + @Test + void determineUsernameFromAddress() { + this.properties.setAddress("user:secret@rabbit.example.com:1234"); + assertThat(this.properties.determineUsername()).isEqualTo("user"); + } + + @Test + void determineUsernameReturnsPropertyWhenAddressHasNoUsername() { + this.properties.setUsername("alice"); + this.properties.setAddress("rabbit.example.com:1234"); + assertThat(this.properties.determineUsername()).isEqualTo("alice"); + } + + @Test + void determineUsernameWithoutPassword() { + this.properties.setAddress("user@rabbit.example.com:1234"); + assertThat(this.properties.determineUsername()).isEqualTo("user"); + assertThat(this.properties.determinePassword()).isEqualTo("guest"); + } + + @Test + void determinePasswordFromAddress() { + this.properties.setAddress("user:secret@rabbit.example.com:1234"); + assertThat(this.properties.determinePassword()).isEqualTo("secret"); + } + + @Test + void determinePasswordReturnsPropertyWhenAddressHasNoPassword() { + this.properties.setPassword("12345678"); + this.properties.setAddress("rabbit.example.com:1234"); + assertThat(this.properties.determinePassword()).isEqualTo("12345678"); + } + + @Test + void determineVirtualHostFromAddress() { + this.properties.setAddress("rabbit.example.com:1234/alpha"); + assertThat(this.properties.determineVirtualHost()).isEqualTo("alpha"); + } + + @Test + void determineVirtualHostReturnsPropertyWhenAddressHasNoVirtualHost() { + this.properties.setVirtualHost("alpha"); + this.properties.setAddress("rabbit.example.com:1234"); + assertThat(this.properties.determineVirtualHost()).isEqualTo("alpha"); + } + + @Test + void determineVirtualHostIsSlashWhenAddressHasTrailingSlash() { + this.properties.setAddress("amqp://root:password@otherhost:1111/"); + assertThat(this.properties.determineVirtualHost()).isEqualTo("/"); + } + + @Test + void ipv6Address() { + this.properties.setAddress("amqp://foo:bar@[aaaa:bbbb:cccc::d]:1234"); + assertThat(this.properties.determineAddress()).isEqualTo("[aaaa:bbbb:cccc::d]:1234"); + assertThat(this.properties.determinePort()).isEqualTo(1234); + } + + @Test + void ipv6AddressDefaultPort() { + this.properties.setAddress("amqp://foo:bar@[aaaa:bbbb:cccc::d]"); + assertThat(this.properties.determineAddress()).isEqualTo("[aaaa:bbbb:cccc::d]:5672"); + assertThat(this.properties.determinePort()).isEqualTo(5672); + } + + @Test + void hostPropertyMustBeSingleHost() { + this.properties.setHost("my-rmq-host.net,my-rmq-host-2.net"); + assertThatExceptionOfType(InvalidConfigurationPropertyValueException.class) + .isThrownBy(this.properties::determineAddress) + .withMessageContaining("spring.amqp.rabbitmq.host"); + } + + @Test + void customSslBundle() { + this.properties.getSsl().setBundle("test-bundle"); + assertThat(this.properties.getSsl().getBundle()).isEqualTo("test-bundle"); + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/PropertiesAmqpRabbitConnectionDetailsTests.java b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/PropertiesAmqpRabbitConnectionDetailsTests.java new file mode 100644 index 00000000000..88d21b6cd27 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/PropertiesAmqpRabbitConnectionDetailsTests.java @@ -0,0 +1,163 @@ +/* + * 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.rabbitmq.autoconfigure; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitConnectionDetails.Address; +import org.springframework.boot.ssl.DefaultSslBundleRegistry; +import org.springframework.boot.ssl.SslBundle; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link PropertiesAmqpRabbitConnectionDetails}. + * + * @author Stephane Nicoll + */ +class PropertiesAmqpRabbitConnectionDetailsTests { + + private AmqpRabbitProperties properties; + + private DefaultSslBundleRegistry sslBundleRegistry; + + private PropertiesAmqpRabbitConnectionDetails connectionDetails; + + @BeforeEach + void setUp() { + this.properties = new AmqpRabbitProperties(); + this.sslBundleRegistry = new DefaultSslBundleRegistry(); + this.connectionDetails = new PropertiesAmqpRabbitConnectionDetails(this.properties, this.sslBundleRegistry); + } + + @Test + void getAddressUsesDefaultHostAndPort() { + Address address = this.connectionDetails.getAddress(); + assertThat(address.host()).isEqualTo("localhost"); + assertThat(address.port()).isEqualTo(5672); + } + + @Test + void getAddressUsesHostAndPortProperties() { + this.properties.setHost("rabbit.example.com"); + this.properties.setPort(1234); + Address address = this.connectionDetails.getAddress(); + assertThat(address.host()).isEqualTo("rabbit.example.com"); + assertThat(address.port()).isEqualTo(1234); + } + + @Test + void getAddressUsesAddressProperty() { + this.properties.setAddress("rabbit.example.com:1234"); + Address address = this.connectionDetails.getAddress(); + assertThat(address.host()).isEqualTo("rabbit.example.com"); + assertThat(address.port()).isEqualTo(1234); + } + + @Test + void getAddressFromAmqpUrl() { + this.properties.setAddress("amqp://user:secret@rabbit.example.com:1234/alpha"); + Address address = this.connectionDetails.getAddress(); + assertThat(address.host()).isEqualTo("rabbit.example.com"); + assertThat(address.port()).isEqualTo(1234); + } + + @Test + void getAddressWithIpv6() { + this.properties.setAddress("amqp://foo:bar@[aaaa:bbbb:cccc::d]:1234"); + Address address = this.connectionDetails.getAddress(); + assertThat(address.host()).isEqualTo("[aaaa:bbbb:cccc::d]"); + assertThat(address.port()).isEqualTo(1234); + } + + @Test + void getUsernameReturnsDefaultGuest() { + assertThat(this.connectionDetails.getUsername()).isEqualTo("guest"); + } + + @Test + void getUsernameUsesProperty() { + this.properties.setUsername("alice"); + assertThat(this.connectionDetails.getUsername()).isEqualTo("alice"); + } + + @Test + void getUsernameFromAddress() { + this.properties.setAddress("user:secret@rabbit.example.com:1234"); + assertThat(this.connectionDetails.getUsername()).isEqualTo("user"); + } + + @Test + void getPasswordReturnsDefaultGuest() { + assertThat(this.connectionDetails.getPassword()).isEqualTo("guest"); + } + + @Test + void getPasswordUsesProperty() { + this.properties.setPassword("secret"); + assertThat(this.connectionDetails.getPassword()).isEqualTo("secret"); + } + + @Test + void getPasswordFromAddress() { + this.properties.setAddress("user:secret@rabbit.example.com:1234"); + assertThat(this.connectionDetails.getPassword()).isEqualTo("secret"); + } + + @Test + void getVirtualHostDefaultsToNull() { + assertThat(this.connectionDetails.getVirtualHost()).isNull(); + } + + @Test + void getVirtualHostUsesProperty() { + this.properties.setVirtualHost("alpha"); + assertThat(this.connectionDetails.getVirtualHost()).isEqualTo("alpha"); + } + + @Test + void getVirtualHostFromAddress() { + this.properties.setAddress("rabbit.example.com:1234/alpha"); + assertThat(this.connectionDetails.getVirtualHost()).isEqualTo("alpha"); + } + + @Test + void getSslBundleDefaultsToNull() { + assertThat(this.connectionDetails.getSslBundle()).isNull(); + } + + @Test + void getSslBundleUsesBundle() { + SslBundle bundle = mock(SslBundle.class); + this.sslBundleRegistry.registerBundle("test-bundle", bundle); + this.properties.getSsl().setBundle("test-bundle"); + assertThat(this.connectionDetails.getSslBundle()).isSameAs(bundle); + } + + @Test + void getSslBundleWithNoSslBundlesThrowsException() { + PropertiesAmqpRabbitConnectionDetails details = new PropertiesAmqpRabbitConnectionDetails(this.properties, + null); + this.properties.getSsl().setBundle("test-bundle"); + assertThatIllegalArgumentException().isThrownBy(details::getSslBundle) + .withMessageContaining("SSL bundle name has been set but no SSL bundles found in context"); + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/RabbitAmqpListenerContainerFactoryConfigurerTests.java b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/RabbitAmqpListenerContainerFactoryConfigurerTests.java new file mode 100644 index 00000000000..16a1aad5153 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/RabbitAmqpListenerContainerFactoryConfigurerTests.java @@ -0,0 +1,106 @@ +/* + * 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.rabbitmq.autoconfigure; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.rabbitmq.client.config.RabbitAmqpListenerContainerFactory; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitProperties.Listener.Batch; +import org.springframework.scheduling.TaskScheduler; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; + +/** + * Tests for {@link RabbitAmqpListenerContainerFactoryConfigurer}. + * + * @author Stephane Nicoll + */ +class RabbitAmqpListenerContainerFactoryConfigurerTests { + + @Test + void configureAppliesDefaultListenerProperties() { + AmqpRabbitProperties properties = new AmqpRabbitProperties(); + RabbitAmqpListenerContainerFactoryConfigurer configurer = new RabbitAmqpListenerContainerFactoryConfigurer( + properties); + RabbitAmqpListenerContainerFactory factory = mock(RabbitAmqpListenerContainerFactory.class); + configurer.configure(factory); + then(factory).should().setObservationEnabled(false); + then(factory).should().setBatchReceiveTimeout(Duration.ofSeconds(30).toMillis()); + then(factory).should(never()).setBatchSize(any()); + then(factory).should(never()).setMessageConverter(any()); + then(factory).should(never()).setTaskScheduler(any()); + } + + @Test + void configureAppliesCustomListenerProperties() { + AmqpRabbitProperties properties = new AmqpRabbitProperties(); + properties.getListener().setObservationEnabled(true); + Batch batch = properties.getListener().getBatch(); + batch.setSize(10); + batch.setReceiveTimeout(Duration.ofSeconds(5)); + RabbitAmqpListenerContainerFactoryConfigurer configurer = new RabbitAmqpListenerContainerFactoryConfigurer( + properties); + RabbitAmqpListenerContainerFactory factory = mock(RabbitAmqpListenerContainerFactory.class); + configurer.configure(factory); + then(factory).should().setObservationEnabled(true); + then(factory).should().setBatchSize(10); + then(factory).should().setBatchReceiveTimeout(Duration.ofSeconds(5).toMillis()); + } + + @Test + void configureAppliesMessageConverter() { + AmqpRabbitProperties properties = new AmqpRabbitProperties(); + RabbitAmqpListenerContainerFactoryConfigurer configurer = new RabbitAmqpListenerContainerFactoryConfigurer( + properties); + MessageConverter messageConverter = mock(MessageConverter.class); + configurer.setMessageConverter(messageConverter); + RabbitAmqpListenerContainerFactory factory = mock(RabbitAmqpListenerContainerFactory.class); + configurer.configure(factory); + then(factory).should().setMessageConverter(messageConverter); + } + + @Test + void configureAppliesTaskScheduler() { + AmqpRabbitProperties properties = new AmqpRabbitProperties(); + RabbitAmqpListenerContainerFactoryConfigurer configurer = new RabbitAmqpListenerContainerFactoryConfigurer( + properties); + TaskScheduler taskScheduler = mock(TaskScheduler.class); + configurer.setTaskScheduler(taskScheduler); + RabbitAmqpListenerContainerFactory factory = mock(RabbitAmqpListenerContainerFactory.class); + configurer.configure(factory); + then(factory).should().setTaskScheduler(taskScheduler); + } + + @Test + void configureSkipsOptionalDependenciesWhenNotSet() { + AmqpRabbitProperties properties = new AmqpRabbitProperties(); + RabbitAmqpListenerContainerFactoryConfigurer configurer = new RabbitAmqpListenerContainerFactoryConfigurer( + properties); + RabbitAmqpListenerContainerFactory factory = mock(RabbitAmqpListenerContainerFactory.class); + configurer.configure(factory); + then(factory).should(never()).setMessageConverter(any()); + then(factory).should(never()).setTaskScheduler(any()); + then(factory).should(never()).setBatchSize(any()); + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/health/AmqpRabbitHealthContributorAutoConfigurationTests.java b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/health/AmqpRabbitHealthContributorAutoConfigurationTests.java new file mode 100644 index 00000000000..3c0ee8b23ec --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/autoconfigure/health/AmqpRabbitHealthContributorAutoConfigurationTests.java @@ -0,0 +1,51 @@ +/* + * 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.rabbitmq.autoconfigure.health; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitAutoConfiguration; +import org.springframework.boot.amqp.rabbitmq.health.AmqpRabbitHealthIndicator; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.health.autoconfigure.contributor.HealthContributorAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AmqpRabbitHealthContributorAutoConfiguration}. + * + * @author Stephane Nicoll + */ +class AmqpRabbitHealthContributorAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AmqpRabbitAutoConfiguration.class, + AmqpRabbitHealthContributorAutoConfiguration.class, HealthContributorAutoConfiguration.class)); + + @Test + void runShouldCreateIndicator() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(AmqpRabbitHealthIndicator.class)); + } + + @Test + void runWhenDisabledShouldNotCreateIndicator() { + this.contextRunner.withPropertyValues("management.health.rabbit.enabled:false") + .run((context) -> assertThat(context).doesNotHaveBean(AmqpRabbitHealthIndicator.class)); + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/docker/compose/RabbitEnvironmentTests.java b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/docker/compose/RabbitEnvironmentTests.java new file mode 100644 index 00000000000..f732c725183 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/docker/compose/RabbitEnvironmentTests.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.rabbitmq.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 RabbitEnvironment}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @author Scott Frederick + */ +class RabbitEnvironmentTests { + + @Test + void getUsernameWhenNoRabbitmqDefaultUser() { + RabbitEnvironment environment = new RabbitEnvironment(Collections.emptyMap()); + assertThat(environment.getUsername()).isEqualTo("guest"); + } + + @Test + void getUsernameWhenHasRabbitmqDefaultUser() { + RabbitEnvironment environment = new RabbitEnvironment(Map.of("RABBITMQ_DEFAULT_USER", "me")); + assertThat(environment.getUsername()).isEqualTo("me"); + } + + @Test + void getUsernameWhenHasRabbitmqUsername() { + RabbitEnvironment environment = new RabbitEnvironment(Map.of("RABBITMQ_USERNAME", "me")); + assertThat(environment.getUsername()).isEqualTo("me"); + } + + @Test + void getPasswordWhenNoRabbitmqDefaultPass() { + RabbitEnvironment environment = new RabbitEnvironment(Collections.emptyMap()); + assertThat(environment.getPassword()).isEqualTo("guest"); + } + + @Test + void getPasswordWhenHasRabbitmqDefaultPass() { + RabbitEnvironment environment = new RabbitEnvironment(Map.of("RABBITMQ_DEFAULT_PASS", "secret")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + + @Test + void getPasswordWhenHasRabbitmqPassword() { + RabbitEnvironment environment = new RabbitEnvironment(Map.of("RABBITMQ_PASSWORD", "secret")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/health/AmqpRabbitHealthIndicatorTests.java b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/health/AmqpRabbitHealthIndicatorTests.java new file mode 100644 index 00000000000..97de985c768 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/rabbitmq/health/AmqpRabbitHealthIndicatorTests.java @@ -0,0 +1,86 @@ +/* + * 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.rabbitmq.health; + +import com.rabbitmq.client.amqp.Connection; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.amqp.rabbitmq.client.AmqpConnectionFactory; +import org.springframework.boot.health.contributor.Health; +import org.springframework.boot.health.contributor.Status; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AmqpRabbitHealthIndicator}. + * + * @author Stephane Nicoll + */ +@ExtendWith(MockitoExtension.class) +class AmqpRabbitHealthIndicatorTests { + + @Mock + @SuppressWarnings("NullAway.Init") + private AmqpConnectionFactory amqpConnectionFactory; + + @Mock + @SuppressWarnings("NullAway.Init") + private Connection connection; + + @Test + @SuppressWarnings("NullAway") // Test null check + void createWhenAmqpConnectionFactoryIsNullShouldThrowException() { + assertThatIllegalArgumentException().isThrownBy(() -> new AmqpRabbitHealthIndicator(null)) + .withMessageContaining("'amqpConnectionFactory' must not be null"); + } + + @Test + void healthWhenConnectionSucceedsShouldReturnUpWithVersion() { + given(this.amqpConnectionFactory.getConnection()).willReturn(this.connection); + Connection.ConnectionInfo connectionInfo = mock(Connection.ConnectionInfo.class); + given(connectionInfo.brokerVersion()).willReturn("123"); + given(this.connection.connectionInfo()).willReturn(connectionInfo); + Health health = new AmqpRabbitHealthIndicator(this.amqpConnectionFactory).health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails()).containsEntry("version", "123"); + } + + @Test + void healthWhenVersionIsMissingShouldReturnUpWithoutVersion() { + given(this.amqpConnectionFactory.getConnection()).willReturn(this.connection); + Connection.ConnectionInfo connectionInfo = mock(Connection.ConnectionInfo.class); + given(connectionInfo.brokerVersion()).willReturn(null); + given(this.connection.connectionInfo()).willReturn(connectionInfo); + Health health = new AmqpRabbitHealthIndicator(this.amqpConnectionFactory).health(); + assertThat(health.getStatus()).isEqualTo(Status.UP); + assertThat(health.getDetails()).doesNotContainKey("version"); + } + + @Test + void healthWhenConnectionFailsShouldReturnDown() { + given(this.amqpConnectionFactory.getConnection()).willThrow(new RuntimeException()); + Health health = new AmqpRabbitHealthIndicator(this.amqpConnectionFactory).health(); + assertThat(health.getStatus()).isEqualTo(Status.DOWN); + } + +} diff --git a/module/spring-boot-autoconfigure-classic-modules/build.gradle b/module/spring-boot-autoconfigure-classic-modules/build.gradle index 355de273613..cb82401cae7 100644 --- a/module/spring-boot-autoconfigure-classic-modules/build.gradle +++ b/module/spring-boot-autoconfigure-classic-modules/build.gradle @@ -31,6 +31,9 @@ dependencies { api(project(":module:spring-boot-amqp")) { transitive = false } + api(project(":module:spring-boot-amqp-rabbitmq")) { + transitive = false + } api(project(":module:spring-boot-artemis")) { transitive = false } diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 525f0501a5a..18a37bcd250 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2063,10 +2063,22 @@ bom { } links { site("https://github.com/rabbitmq/rabbitmq-java-client") - javadoc("https://rabbitmq.github.io/rabbitmq-java-client/api/current", "com.rabbitmq") + javadoc("https://rabbitmq.github.io/rabbitmq-java-client/api/current", "com.rabbitmq.client") releaseNotes("https://github.com/rabbitmq/rabbitmq-java-client/releases/tag/v{version}") } } + library("RabbitMQ AMQP Client", "1.2.0") { + group("com.rabbitmq.client") { + modules = [ + "amqp-client" + ] + } + links { + site("https://github.com/rabbitmq/rabbitmq-amqp-java-client") + javadoc("https://rabbitmq.github.io/rabbitmq-amqp-java-client/stable/api", "com.rabbitmq.amqp.client") + releaseNotes("https://github.com/rabbitmq/rabbitmq-amqp-java-client/releases/tag/v{version}") + } + } library("RabbitMQ Stream Client", "1.6.0") { group("com.rabbitmq") { modules = [ @@ -2124,6 +2136,7 @@ bom { "spring-boot-actuator", "spring-boot-actuator-autoconfigure", "spring-boot-amqp", + "spring-boot-amqp-rabbitmq", "spring-boot-artemis", "spring-boot-autoconfigure", "spring-boot-autoconfigure-classic", @@ -2246,6 +2259,8 @@ bom { "spring-boot-starter-actuator", "spring-boot-starter-actuator-test", "spring-boot-starter-amqp", + "spring-boot-starter-amqp-rabbitmq", + "spring-boot-starter-amqp-rabbitmq-test", "spring-boot-starter-amqp-test", "spring-boot-starter-artemis", "spring-boot-starter-artemis-test", diff --git a/settings.gradle b/settings.gradle index 269c5fccec1..4a5e437de32 100644 --- a/settings.gradle +++ b/settings.gradle @@ -81,6 +81,7 @@ 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-amqp-rabbitmq" include "module:spring-boot-artemis" include "module:spring-boot-autoconfigure-classic" include "module:spring-boot-autoconfigure-classic-modules" @@ -213,6 +214,8 @@ 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-rabbitmq" +include "starter:spring-boot-starter-amqp-rabbitmq-test" include "starter:spring-boot-starter-amqp-test" include "starter:spring-boot-starter-artemis" include "starter:spring-boot-starter-artemis-test" @@ -405,6 +408,7 @@ include ":smoke-test:spring-boot-smoke-test-actuator-log4j2" include ":smoke-test:spring-boot-smoke-test-actuator-noweb" include ":smoke-test:spring-boot-smoke-test-actuator-ui" include ":smoke-test:spring-boot-smoke-test-amqp" +include ":smoke-test:spring-boot-smoke-test-amqp-rabbitmq" include ":smoke-test:spring-boot-smoke-test-ant" include ":smoke-test:spring-boot-smoke-test-artemis" include ":smoke-test:spring-boot-smoke-test-aspectj" diff --git a/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/build.gradle b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/build.gradle new file mode 100644 index 00000000000..ac586791eb0 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/build.gradle @@ -0,0 +1,41 @@ +/* + * 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" + id "org.springframework.boot.docker-test" +} + +description = "Spring Boot RabbitMQ AMQP smoke test" + +dependencies { + implementation(project(":starter:spring-boot-starter-amqp-rabbitmq")) + + dockerTestImplementation(project(":starter:spring-boot-starter-test")) + dockerTestImplementation(project(":core:spring-boot-testcontainers")) + dockerTestImplementation(project(":test-support:spring-boot-docker-test-support")) + dockerTestImplementation("org.awaitility:awaitility") + dockerTestImplementation("org.testcontainers:testcontainers-junit-jupiter") + dockerTestImplementation("org.testcontainers:testcontainers-rabbitmq") +} + +tasks.named("compileTestJava") { + options.nullability.checking = "tests" +} + +tasks.named("compileDockerTestJava") { + options.nullability.checking = "tests" +} \ No newline at end of file diff --git a/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/dockerTest/java/smoketest/amqp/rabbitmq/SampleRabbitAmqpSimpleApplicationTests.java b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/dockerTest/java/smoketest/amqp/rabbitmq/SampleRabbitAmqpSimpleApplicationTests.java new file mode 100644 index 00000000000..e0c7f8e6853 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/dockerTest/java/smoketest/amqp/rabbitmq/SampleRabbitAmqpSimpleApplicationTests.java @@ -0,0 +1,56 @@ +/* + * 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 smoketest.amqp.rabbitmq; + +import java.time.Duration; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.rabbitmq.RabbitMQContainer; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.testsupport.container.TestImage; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@Testcontainers(disabledWithoutDocker = true) +@ExtendWith(OutputCaptureExtension.class) +class SampleRabbitAmqpSimpleApplicationTests { + + @Container + @ServiceConnection + static final RabbitMQContainer rabbit = TestImage.container(RabbitMQContainer.class); + + @Autowired + private Sender sender; + + @Test + void sendSimpleMessage(CapturedOutput output) { + this.sender.send("Test message"); + Awaitility.waitAtMost(Duration.ofMinutes(1)) + .untilAsserted(() -> assertThat(output).contains("'Test message'").contains("'smoke-test'")); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/rabbitmq/SampleRabbitAmqpSimpleApplication.java b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/rabbitmq/SampleRabbitAmqpSimpleApplication.java new file mode 100644 index 00000000000..83c73d1ed34 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/rabbitmq/SampleRabbitAmqpSimpleApplication.java @@ -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. + */ + +package smoketest.amqp.rabbitmq; + +import com.rabbitmq.client.amqp.Message; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +public class SampleRabbitAmqpSimpleApplication { + + private static final Log logger = LogFactory.getLog(SampleRabbitAmqpSimpleApplication.class); + + @Bean + public Sender mySender() { + return new Sender(); + } + + @Bean + public Queue fooQueue() { + return new Queue("foo"); + } + + @RabbitListener(queues = "foo") + public void process(Message amqpMessage) { + String body = new String(amqpMessage.body()); + String sourceApp = (String) amqpMessage.property("source-app"); + logger.info("Received '%s' from '%s'".formatted(body, sourceApp)); + } + + @Bean + public ApplicationRunner runner(Sender sender) { + return (args) -> sender.send("Hello"); + } + + public static void main(String[] args) { + SpringApplication.run(SampleRabbitAmqpSimpleApplication.class, args); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/rabbitmq/Sender.java b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/rabbitmq/Sender.java new file mode 100644 index 00000000000..d8b873d8972 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/rabbitmq/Sender.java @@ -0,0 +1,34 @@ +/* + * 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 smoketest.amqp.rabbitmq; + +import org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate; +import org.springframework.beans.factory.annotation.Autowired; + +public class Sender { + + @Autowired + private RabbitAmqpTemplate rabbitAmqpTemplate; + + public void send(String payload) { + this.rabbitAmqpTemplate.convertAndSend("foo", payload, (message) -> { + message.getMessageProperties().setHeader("source-app", "smoke-test"); + return message; + }); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/rabbitmq/package-info.java b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/rabbitmq/package-info.java new file mode 100644 index 00000000000..62bf09a6b3d --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/rabbitmq/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +@NullMarked +package smoketest.amqp.rabbitmq; + +import org.jspecify.annotations.NullMarked; diff --git a/starter/spring-boot-starter-amqp-rabbitmq-test/build.gradle b/starter/spring-boot-starter-amqp-rabbitmq-test/build.gradle new file mode 100644 index 00000000000..c4e673e9c22 --- /dev/null +++ b/starter/spring-boot-starter-amqp-rabbitmq-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 Spring AMQP with Rabbit MQ over AMQP 1.0 protocol" + +dependencies { + api(project(":starter:spring-boot-starter-amqp-rabbitmq")) + api(project(":starter:spring-boot-starter-test")) +} diff --git a/starter/spring-boot-starter-amqp-rabbitmq/build.gradle b/starter/spring-boot-starter-amqp-rabbitmq/build.gradle new file mode 100644 index 00000000000..cb36be1a7d1 --- /dev/null +++ b/starter/spring-boot-starter-amqp-rabbitmq/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 using Spring AMQP with Rabbit MQ over AMQP 1.0 protocol" + +dependencies { + api(project(":starter:spring-boot-starter")) + + api(project(":module:spring-boot-amqp-rabbitmq")) +}