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/reference/pages/messaging/amqp.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/messaging/amqp.adoc index 4aad8ad6290..235a7f0170b 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 @@ -213,3 +213,85 @@ You can also customize the javadoc:org.springframework.core.retry.RetryPolicy[] IMPORTANT: By default, if retries are disabled and the listener throws an exception, the delivery is retried indefinitely. You can modify this behavior in two ways: Set the `defaultRequeueRejected` property to `false` so that zero re-deliveries are attempted or throw an javadoc:org.springframework.amqp.AmqpRejectAndDontRequeueException[] to signal the message should be rejected. The latter is the mechanism used when retries are enabled and the maximum number of delivery attempts is reached. + + + +[[messaging.amqp.rabbitmq-amqp]] +== RabbitMQ AMQP 1.0 Support + +https://www.rabbitmq.com/[RabbitMQ] supports the AMQP 1.0 protocol via the https://github.com/rabbitmq/rabbitmq-amqp-java-client[RabbitMQ AMQP 1.0 Java client]. +Spring AMQP provides support for this client through `spring-rabbitmq-client`. +Spring Boot offers auto-configuration for this support via the `spring-boot-starter-amqp-rabbitmq` starter. + +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. + +See javadoc:org.springframework.boot.amqp.autoconfigure.RabbitAmqpProperties[] 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.autoconfigure.RabbitAmqpEnvironmentBuilderCustomizer[] 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-amqp.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" +---- + +You can also customize the javadoc:org.springframework.amqp.rabbitmq.client.RabbitAmqpTemplate[] programmatically by declaring a javadoc:org.springframework.boot.amqp.autoconfigure.RabbitAmqpTemplateCustomizer[] bean. + + + +[[messaging.amqp.rabbitmq-amqp.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. + +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. + +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[]. diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmqamqp/receiving/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmqamqp/receiving/MyBean.java new file mode 100644 index 00000000000..83ac5b882b0 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmqamqp/receiving/MyBean.java @@ -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.rabbitmqamqp.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 { + + @RabbitHandler + public void processMessage(String content) { + // ... + } + +} diff --git a/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmqamqp/sending/MyBean.java b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmqamqp/sending/MyBean.java new file mode 100644 index 00000000000..a05a238bf68 --- /dev/null +++ b/documentation/spring-boot-docs/src/main/java/org/springframework/boot/docs/messaging/amqp/rabbitmqamqp/sending/MyBean.java @@ -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. + */ + +package org.springframework.boot.docs.messaging.amqp.rabbitmqamqp.sending; + +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 RabbitAmqpAdmin amqpAdmin; + + private final RabbitAmqpTemplate amqpTemplate; + + public MyBean(RabbitAmqpAdmin amqpAdmin, RabbitAmqpTemplate amqpTemplate) { + this.amqpAdmin = amqpAdmin; + this.amqpTemplate = amqpTemplate; + } + + // @fold:on // ... + public void someOtherMethod() { + this.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..edd172e77c6 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/build.gradle @@ -0,0 +1,65 @@ +/* + * 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" + +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(project(":module:spring-boot-micrometer-metrics")) + optional("io.micrometer:micrometer-core") + optional("org.springframework.amqp:spring-rabbit-stream") + 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/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 00000000000..77baad2a4bf --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.docker.compose; + +import org.springframework.boot.amqp.autoconfigure.RabbitAmqpConnectionDetails; +import org.springframework.boot.amqp.autoconfigure.RabbitAmqpConnectionDetails.Address; +import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest; +import org.springframework.boot.testsupport.container.TestImage; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link RabbitDockerComposeConnectionDetailsFactory}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @author Scott Frederick + * @author Eddú Meléndez + */ +class RabbitDockerComposeConnectionDetailsFactoryIntegrationTests { + + @DockerComposeTest(composeFile = "rabbit-compose.yaml", image = TestImage.RABBITMQ) + void runCreatesConnectionDetails(RabbitAmqpConnectionDetails connectionDetails) { + assertThat(connectionDetails.getUsername()).isEqualTo("myuser"); + assertThat(connectionDetails.getPassword()).isEqualTo("secret"); + assertThat(connectionDetails.getVirtualHost()).isEqualTo("/"); + assertThat(connectionDetails.getAddress()).isNotNull(); + 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/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java b/module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.java new file mode 100644 index 00000000000..34487fddcf6 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactoryIntegrationTests.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.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.autoconfigure.RabbitAmqpAutoConfiguration; +import org.springframework.boot.amqp.autoconfigure.RabbitAmqpConnectionDetails; +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 RabbitContainerConnectionDetailsFactory}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @author Eddú Meléndez + */ +@SpringJUnitConfig +@Testcontainers(disabledWithoutDocker = true) +class RabbitContainerConnectionDetailsFactoryIntegrationTests { + + @Container + @ServiceConnection + static final RabbitMQContainer rabbit = TestImage.container(RabbitMQContainer.class); + + @Autowired(required = false) + private RabbitAmqpConnectionDetails 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(RabbitAmqpAutoConfiguration.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/docker/compose/rabbit-compose.yaml b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-compose.yaml new file mode 100644 index 00000000000..1951fba4bb0 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/dockerTest/resources/org/springframework/boot/amqp/docker/compose/rabbit-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/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/autoconfigure/PropertiesRabbitAmqpConnectionDetails.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitAmqpConnectionDetails.java new file mode 100644 index 00000000000..2eb06294eb9 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/PropertiesRabbitAmqpConnectionDetails.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import org.jspecify.annotations.Nullable; + +/** + * Adapts {@link RabbitAmqpProperties} to {@link RabbitAmqpConnectionDetails}. + * + * @author Eddú Meléndez + */ +class PropertiesRabbitAmqpConnectionDetails implements RabbitAmqpConnectionDetails { + + private final RabbitAmqpProperties properties; + + PropertiesRabbitAmqpConnectionDetails(RabbitAmqpProperties properties) { + this.properties = properties; + } + + @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 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)); + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpAutoConfiguration.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpAutoConfiguration.java new file mode 100644 index 00000000000..a433060acc3 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpAutoConfiguration.java @@ -0,0 +1,132 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import 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.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.SingleAmqpConnectionFactory; +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.amqp.autoconfigure.RabbitAmqpConnectionDetails.Address; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for {@link RabbitAmqpTemplate}. + * + * @author Eddú Meléndez + * @since 4.1.0 + */ +@AutoConfiguration +@ConditionalOnClass({ RabbitAmqpTemplate.class, Connection.class }) +@EnableConfigurationProperties(RabbitAmqpProperties.class) +@Import(RabbitAnnotationDrivenConfiguration.class) +public final class RabbitAmqpAutoConfiguration { + + private final RabbitAmqpProperties properties; + + RabbitAmqpAutoConfiguration(RabbitAmqpProperties properties) { + this.properties = properties; + } + + @Bean + @ConditionalOnMissingBean + RabbitAmqpConnectionDetails rabbitConnectionDetails() { + return new PropertiesRabbitAmqpConnectionDetails(this.properties); + } + + @Bean(name = "rabbitListenerContainerFactory") + @ConditionalOnMissingBean(name = "rabbitListenerContainerFactory") + RabbitAmqpListenerContainerFactory rabbitAmqpListenerContainerFactory(AmqpConnectionFactory connectionFactory, + ObjectProvider> amqpContainerCustomizer) { + RabbitAmqpListenerContainerFactory factory = new RabbitAmqpListenerContainerFactory(connectionFactory); + amqpContainerCustomizer.ifUnique(factory::setContainerCustomizer); + + RabbitAmqpProperties.AmqpContainer configuration = this.properties.getListener().getAmqp(); + factory.setObservationEnabled(configuration.isObservationEnabled()); + return factory; + } + + @Bean + @ConditionalOnMissingBean + Environment rabbitAmqpEnvironment(RabbitAmqpConnectionDetails 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); + + AmqpEnvironmentBuilder builder = environmentConnectionSettings.environmentBuilder(); + customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); + return builder.build(); + } + + @Bean + @ConditionalOnMissingBean + AmqpConnectionFactory amqpConnectionFactory(Environment environment) { + return new SingleAmqpConnectionFactory(environment); + } + + @Bean + @ConditionalOnMissingBean + RabbitAmqpTemplate rabbitAmqpTemplate(AmqpConnectionFactory connectionFactory, + ObjectProvider customizers, + ObjectProvider messageConverter) { + RabbitAmqpTemplate rabbitAmqpTemplate = new RabbitAmqpTemplate(connectionFactory); + if (messageConverter.getIfAvailable() != null) { + rabbitAmqpTemplate.setMessageConverter(messageConverter.getIfAvailable()); + } + RabbitAmqpProperties.Template templateProperties = this.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/autoconfigure/RabbitAmqpConnectionDetails.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpConnectionDetails.java new file mode 100644 index 00000000000..344909fffb0 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpConnectionDetails.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; + +/** + * Details required to establish a connection to a RabbitMQ AMQP service. + * + * @author Eddú Meléndez + * @since 4.1.0 + */ +public interface RabbitAmqpConnectionDetails extends ConnectionDetails { + + /** + * 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; + } + + /** + * Login to authenticate against the broker. + * @return the login to authenticate against 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; + } + + /** + * Returns the address. + * @return the address + * @throws IllegalStateException if the address list is empty + */ + Address getAddress(); + + /** + * 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/autoconfigure/RabbitAmqpEnvironmentBuilderCustomizer.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpEnvironmentBuilderCustomizer.java new file mode 100644 index 00000000000..807ead65def --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpEnvironmentBuilderCustomizer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import 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 {@link Environment} that is created by an + * {@link AmqpEnvironmentBuilder}. + * + * @author Eddú Meléndez + * @since 4.1.0 + */ +@FunctionalInterface +public interface RabbitAmqpEnvironmentBuilderCustomizer { + + /** + * Customize the {@code AmqpEnvironmentBuilder}. + * @param builder the builder to customize + */ + void customize(AmqpEnvironmentBuilder builder); + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpProperties.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpProperties.java new file mode 100644 index 00000000000..87ddff5492b --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpProperties.java @@ -0,0 +1,383 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.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.1.0 + */ +@ConfigurationProperties("spring.amqp.rabbitmq") +public class RabbitAmqpProperties { + + private 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; + + /** + * Listener container configuration. + */ + private final Listener listener = new Listener(); + + 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.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 Listener getListener() { + return this.listener; + } + + public Template getTemplate() { + return this.template; + } + + public static class Listener { + + private final AmqpContainer amqp = new AmqpContainer(); + + public AmqpContainer getAmqp() { + return this.amqp; + } + + } + + /** + * Configuration properties for {@code RabbitAmqpListenerContainer}. + */ + public static class AmqpContainer { + + /** + * Whether to enable observation. + */ + private boolean observationEnabled; + + /** + * Batch size, expressed as the number of physical messages, to be used by the + * container. + */ + private @Nullable Integer batchSize; + + public boolean isObservationEnabled() { + return this.observationEnabled; + } + + public void setObservationEnabled(boolean observationEnabled) { + this.observationEnabled = observationEnabled; + } + + public @Nullable Integer getBatchSize() { + return this.batchSize; + } + + public void setBatchSize(@Nullable Integer batchSize) { + this.batchSize = batchSize; + } + + } + + 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/autoconfigure/RabbitAmqpTemplateCustomizer.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpTemplateCustomizer.java new file mode 100644 index 00000000000..e1eb2404cbb --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/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.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.0.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/autoconfigure/RabbitAnnotationDrivenConfiguration.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAnnotationDrivenConfiguration.java new file mode 100644 index 00000000000..c2c766c096e --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitAnnotationDrivenConfiguration.java @@ -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. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import org.springframework.amqp.rabbit.annotation.EnableRabbit; +import org.springframework.amqp.rabbit.config.RabbitListenerConfigUtils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Configuration; + +/** + * Configuration for Spring AMQP annotation driven endpoints. + * + * @author Eddú Meléndez + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(EnableRabbit.class) +class RabbitAnnotationDrivenConfiguration { + + @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/autoconfigure/package-info.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java new file mode 100644 index 00000000000..b519c35a2ca --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/autoconfigure/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for RabbitMQ. + */ +@NullMarked +package org.springframework.boot.amqp.autoconfigure; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java new file mode 100644 index 00000000000..289fbe1cd70 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,91 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.docker.compose; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.amqp.autoconfigure.RabbitAmqpConnectionDetails; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create + * {@link RabbitAmqpConnectionDetails} for a {@code rabbitmq} service. + * + * @author Eddú Meléndez + */ +class RabbitDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { + + private static final int RABBITMQ_PORT = 5672; + + protected RabbitDockerComposeConnectionDetailsFactory() { + super("rabbitmq"); + } + + @Override + protected @Nullable RabbitAmqpConnectionDetails getDockerComposeConnectionDetails( + DockerComposeConnectionSource source) { + try { + return new RabbitAmqpDockerComposeConnectionDetails(source.getRunningService()); + } + catch (IllegalStateException ex) { + return null; + } + } + + /** + * {@link RabbitAmqpConnectionDetails} backed by a {@code rabbitmq} + * {@link RunningService}. + */ + static class RabbitAmqpDockerComposeConnectionDetails extends DockerComposeConnectionDetails + implements RabbitAmqpConnectionDetails { + + private final RabbitEnvironment environment; + + private final Address address; + + protected RabbitAmqpDockerComposeConnectionDetails(RunningService service) { + super(service); + this.environment = new RabbitEnvironment(service.env()); + this.address = new Address(service.host(), service.ports().get(RABBITMQ_PORT)); + } + + @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 Address getAddress() { + return this.address; + } + + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironment.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironment.java new file mode 100644 index 00000000000..bc776b015db --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironment.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.docker.compose; + +import java.util.Map; + +import org.jspecify.annotations.Nullable; + +/** + * RabbitMQ environment details. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @author Scott Frederick + */ +class 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/docker/compose/package-info.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java new file mode 100644 index 00000000000..c2d355c0797 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/docker/compose/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Support for Docker Compose RabbitMQ service connections. + */ +@NullMarked +package org.springframework.boot.amqp.docker.compose; + +import org.jspecify.annotations.NullMarked; diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactory.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactory.java new file mode 100644 index 00000000000..58d6560e6b8 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/RabbitContainerConnectionDetailsFactory.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.testcontainers; + +import java.net.URI; + +import org.jspecify.annotations.Nullable; +import org.testcontainers.rabbitmq.RabbitMQContainer; + +import org.springframework.boot.amqp.autoconfigure.RabbitAmqpConnectionDetails; +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 RabbitAmqpConnectionDetails} + * from a {@link ServiceConnection @ServiceConnection}-annotated + * {@link RabbitMQContainer}. + * + * @author Eddú Meléndez + */ +class RabbitContainerConnectionDetailsFactory + extends ContainerConnectionDetailsFactory { + + @Override + protected RabbitAmqpConnectionDetails getContainerConnectionDetails( + ContainerConnectionSource source) { + return new RabbitAmqpMqContainerConnectionDetails(source); + } + + /** + * {@link RabbitAmqpConnectionDetails} backed by a {@link ContainerConnectionSource}. + */ + static final class RabbitAmqpMqContainerConnectionDetails extends ContainerConnectionDetails + implements RabbitAmqpConnectionDetails { + + private RabbitAmqpMqContainerConnectionDetails(ContainerConnectionSource source) { + super(source); + } + + @Override + public String getUsername() { + return getContainer().getAdminUsername(); + } + + @Override + public String getPassword() { + return getContainer().getAdminPassword(); + } + + @Override + public Address getAddress() { + URI uri = URI.create((getSslBundle() != null) ? getContainer().getAmqpsUrl() : getContainer().getAmqpUrl()); + return new Address(uri.getHost(), uri.getPort()); + } + + @Override + public @Nullable SslBundle getSslBundle() { + return super.getSslBundle(); + } + + } + +} diff --git a/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java new file mode 100644 index 00000000000..69c6364ef32 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/java/org/springframework/boot/amqp/testcontainers/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Support for testcontainers RabbitMQ service connections. + */ +@NullMarked +package org.springframework.boot.amqp.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..412b6813fab --- /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.docker.compose.RabbitDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.amqp.testcontainers.RabbitContainerConnectionDetailsFactory 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..050f5729e28 --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.springframework.boot.amqp.autoconfigure.RabbitAmqpAutoConfiguration \ No newline at end of file diff --git a/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpAutoConfigurationTests.java b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpAutoConfigurationTests.java new file mode 100644 index 00000000000..e1c5835ff6c --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitAmqpAutoConfigurationTests.java @@ -0,0 +1,160 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.amqp.autoconfigure; + +import org.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.autoconfigure.AutoConfigurations; +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.BDDMockito.then; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link RabbitAmqpAutoConfiguration}. + * + * @author Eddú Meléndez + */ +class RabbitAmqpAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(RabbitAmqpAutoConfiguration.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(); + }); + } + + @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/docker/compose/RabbitEnvironmentTests.java b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/docker/compose/RabbitEnvironmentTests.java new file mode 100644 index 00000000000..1ecb0b4d6ff --- /dev/null +++ b/module/spring-boot-amqp-rabbitmq/src/test/java/org/springframework/boot/amqp/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.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 getUsernameWhenNoRabbitmqDefaultPass() { + RabbitEnvironment environment = new RabbitEnvironment(Collections.emptyMap()); + assertThat(environment.getPassword()).isEqualTo("guest"); + } + + @Test + void getUsernameWhenHasRabbitmqDefaultPass() { + RabbitEnvironment environment = new RabbitEnvironment(Map.of("RABBITMQ_DEFAULT_PASS", "secret")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + + @Test + void getUsernameWhenHasRabbitmqPassword() { + RabbitEnvironment environment = new RabbitEnvironment(Map.of("RABBITMQ_PASSWORD", "secret")); + assertThat(environment.getPassword()).isEqualTo("secret"); + } + +} diff --git a/platform/spring-boot-dependencies/build.gradle b/platform/spring-boot-dependencies/build.gradle index 525f0501a5a..a89833cb5a0 100644 --- a/platform/spring-boot-dependencies/build.gradle +++ b/platform/spring-boot-dependencies/build.gradle @@ -2124,6 +2124,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 +2247,7 @@ bom { "spring-boot-starter-actuator", "spring-boot-starter-actuator-test", "spring-boot-starter-amqp", + "spring-boot-starter-amqp-rabbitmq", "spring-boot-starter-amqp-test", "spring-boot-starter-artemis", "spring-boot-starter-artemis-test", diff --git a/settings.gradle b/settings.gradle index 269c5fccec1..ce1e29e4c9b 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,7 @@ 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-test" include "starter:spring-boot-starter-artemis" include "starter:spring-boot-starter-artemis-test" @@ -405,6 +407,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/SampleRabbitAmqpSimpleApplicationTests.java b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/dockerTest/java/smoketest/amqp/SampleRabbitAmqpSimpleApplicationTests.java new file mode 100644 index 00000000000..bd9f51c562a --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/dockerTest/java/smoketest/amqp/SampleRabbitAmqpSimpleApplicationTests.java @@ -0,0 +1,55 @@ +/* + * 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; + +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")); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/SampleRabbitAmqpSimpleApplication.java b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/SampleRabbitAmqpSimpleApplication.java new file mode 100644 index 00000000000..aff9bc342e3 --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/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; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.annotation.RabbitHandler; +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; +import org.springframework.messaging.handler.annotation.Payload; + +@SpringBootApplication +@RabbitListener(queues = "foo") +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"); + } + + @RabbitHandler + public void process(@Payload String foo) { + logger.info(foo); + } + + @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/Sender.java b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/Sender.java new file mode 100644 index 00000000000..6639bc0316c --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/Sender.java @@ -0,0 +1,31 @@ +/* + * 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; + +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 message) { + this.rabbitAmqpTemplate.convertAndSend("foo", message); + } + +} diff --git a/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/package-info.java b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/package-info.java new file mode 100644 index 00000000000..bc9e41a079d --- /dev/null +++ b/smoke-test/spring-boot-smoke-test-amqp-rabbitmq/src/main/java/smoketest/amqp/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; + +import org.jspecify.annotations.NullMarked; 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")) +}